使用 MongoDB、Express 框架和 TypeScript 构建 URL 缩短器
这篇文章最初发表在我的博客上。
大家好,我在上一篇文章中讨论了 Express 框架和 TypeScript。在本文中,我将沿用这种结构。
所以,我不会谈论我们将采用什么样的结构。
开始之前
在开始之前,我们将使用 MongoDB 来进行这个项目,为了获取环境变量值,我们将使用 dotenv 包。
nodemon:Nick Taylor向我推荐的。使用 nodemon,您无需手动停止和启动应用程序,它会自动为您完成这些操作。
mongoose:用于连接 MongoDB 的驱动程序。
dotenv:一个用于获取环境变量值的软件包。
安装软件包
npm i typescript nodemon express mongoose pug ts-node dotenv @types/node @types/mongoose @types/express
让我们编辑package.json文件中的scripts部分。
"scripts": {
"dev": "nodemon src/server.ts",
"start": "ts-node dist/server.js",
"build": "tsc -p ."
}
tsconfig.json
{
"compilerOptions": {
"sourceMap": true,
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"baseUrl": "./src"
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules"
]
}
让我们创建一个项目结构
民众
CSS
在这个文件夹中,我们将有两个 CSS 文件,分别名为bootstrap.css和app.css。bootstrap.css文件将使用 Bootstrap 4.x 版本,而 app.css 文件将用于自定义样式。
app.css
.right {
float: inline-end;
}
js
在这个文件夹中,我们会有一个名为 app.js 的文件。客户端操作将在这里进行。
app.js
const btnShort = document.getElementById('btn-short')
const url = document.getElementById('url')
const urlAlert = document.getElementById('url-alert')
const urlAlertText = document.getElementById('url-alert-text')
const validURL = (str) => {
const pattern = new RegExp('^(https?:\\/\\/)?'+
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+
'((\\d{1,3}\\.){3}\\d{1,3}))'+
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+
'(\\?[;&a-z\\d%_.~+=-]*)?'+
'(\\#[-a-z\\d_]*)?$','i');
return !!pattern.test(str);
}
function saveClipBoard(data) {
var dummy = document.createElement('input');
var text = data;
document.body.appendChild(dummy);
dummy.value = text;
dummy.select();
var success = document.execCommand('copy');
document.body.removeChild(dummy);
return success;
}
const shortenerResponse = (isValidUrl, serverMessage) => {
let message = ''
if (isValidUrl) {
urlAlert.classList.remove('alert-danger')
urlAlert.classList.add('alert-success')
urlAlert.classList.remove('invisible')
message = `
<strong>Your URL:</strong>
<a id="shorted-url" href="${serverMessage}" target="_blank">${serverMessage}</a>
<button class="btn btn-sm btn-primary right" id="btn-copy-link">Copy</button>
<span class="mr-2 right d-none" id="copied">Copied</span>
`
} else {
urlAlert.classList.remove('alert-success')
urlAlert.classList.add('alert-danger')
urlAlert.classList.remove('invisible')
message = `<strong>Warning:</strong> ${serverMessage}`
}
urlAlertText.innerHTML = message
}
url.addEventListener('keypress', (e) => {
if (e.which == 13 || e.keyCode == 13 || e.key == 'Enter') {
btnShort.click()
}
})
btnShort.addEventListener('click', async () => {
const longUrl = url.value
const isValidUrl = validURL(longUrl)
if(isValidUrl) {
const response = await fetch('/create', {
method: 'POST',
body: JSON.stringify({
url: longUrl
}),
headers: {
'Content-Type': 'application/json'
}
}).then(resp => resp.json())
let success = response.success
let message = ''
if(success) {
const { url } = response
message = `${window.location.origin}/${url}`
} else {
message = `URL couldn't shortened`
}
shortenerResponse(success, message)
} else {
shortenerResponse(isValidUrl, 'Please enter a correct URL')
}
})
document.addEventListener('click', (e) => {
if (e.target && e.target.id == 'btn-copy-link') {
const shortedUrl = document.getElementById("shorted-url")
const isCopied = saveClipBoard(shortedUrl.href)
if (isCopied) {
document.getElementById('copied').classList.remove('d-none')
}
}
})
来源
控制器
这个文件夹里将包含控制器及其模型和接口文件。
controllers/shortener.controller.ts
在这个控制器中,我们将插入一个指向 MongoDB 数据库的长 URL。顺便一提,我们当时还没有建立 MongoDB 连接。
generateRandomUrl:一个用于生成随机字符的私有方法。它接受一个字符长度的数字作为参数。
index:用于显示首页的异步方法。
get:一个用于获取短链接信息的异步方法。它接受一个短链接代码作为参数。例如:http://example.com/abc12
create:一个用于缩短长 URL 的异步方法。首先,它会查找长 URL。如果存在,则会在 MongoDB 中显示相应的短代码。
使用shorterModel,我们可以将文档保存到MongoDB并在MongoDB中进行搜索。
import * as express from 'express'
import { Request, Response } from 'express'
import IControllerBase from 'interfaces/IControllerBase.interface'
import shortenerModel from './shortener.model'
import IShortener from './shortener.interface';
class ShortenerController implements IControllerBase {
public path = '/'
public router = express.Router()
constructor() {
this.initRoutes()
}
public initRoutes() {
this.router.get('/', this.index)
this.router.get('/:shortcode', this.get)
this.router.post('/create', this.create)
}
private generateRandomUrl(length: Number) {
const possibleChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let urlChars = "";
for (var i = 0; i < length; i++) {
urlChars += possibleChars.charAt(Math.floor(Math.random() * possibleChars.length));
}
return urlChars;
}
index = async(req: Request, res: Response) => {
res.render('home/index')
}
get = async(req: Request, res: Response) => {
const { shortcode } = req.params
const data: IShortener = {
shortUrl: shortcode
}
const urlInfo = await shortenerModel.findOne(data)
if (urlInfo != null) {
res.redirect(302, urlInfo.longUrl)
} else {
res.render('home/not-found')
}
}
create = async(req: express.Request, res: express.Response) => {
const { url } = req.body
const data: IShortener = {
longUrl: url
}
let urlInfo = await shortenerModel.findOne(data)
if (urlInfo == null) {
const shortCode = this.generateRandomUrl(5)
const shortData: IShortener = {
longUrl: url,
shortUrl: shortCode
}
const shortenerData = new shortenerModel(shortData)
urlInfo = await shortenerData.save()
}
res.json({
success: true,
message: 'URL Shortened',
url: urlInfo.shortUrl
})
}
}
export default ShortenerController
controllers/shortener.interface.ts
在这个接口中,我们使用名为 ISHortener 的接口。它有两个可选参数。
interface IShortener {
longUrl?: string,
shortUrl?: string
}
export default IShortener
controllers/shortener.model.ts
在这个文件中,我们正在构建一个 Mongoose schema。它有两个可选参数,例如 ` shortener.interface.ts`。此外,该模型需要 `IShortener`。
import * as mongoose from 'mongoose'
import IShortener from './shortener.interface'
const shortenerSchema = new mongoose.Schema({
longUrl: String,
shortUrl: String
})
const shortenerModel = mongoose.model<IShortener & mongoose.Document>('Shortener', shortenerSchema);
export default shortenerModel;
接口
在这个文件夹中,我们只会有一个接口文件,名为IControllerBase。
interfaces/IControllerBase.interface.ts
interface IControllerBase {
initRoutes(): any
}
export default IControllerBase
中间件
这里什么都没有,我们创建这个文件夹是为了以防您需要中间件。
src/app.ts
在这个文件中,我们将连接到 MongoDB。我们还将使用dotenv来获取环境变量。
initDatabase:我们正在连接 MongoDB。
import * as express from 'express'
import { Application } from 'express'
import * as mongoose from 'mongoose';
import 'dotenv/config';
class App {
public app: Application
public port: number
constructor(appInit: { port: number; middleWares: any; controllers: any; }) {
this.app = express()
this.port = appInit.port
this.initDatabase()
this.middlewares(appInit.middleWares)
this.routes(appInit.controllers)
this.assets()
this.template()
}
private middlewares(middleWares: { forEach: (arg0: (middleWare: any) => void) => void; }) {
middleWares.forEach(middleWare => {
this.app.use(middleWare)
})
}
private routes(controllers: { forEach: (arg0: (controller: any) => void) => void; }) {
controllers.forEach(controller => {
this.app.use('/', controller.router)
})
}
private initDatabase() {
const {
MONGO_USER,
MONGO_PASSWORD,
MONGO_PATH
} = process.env
mongoose.connect(`mongodb+srv://${MONGO_USER}:${MONGO_PASSWORD}${MONGO_PATH}`, {
useCreateIndex: true,
useNewUrlParser: true,
useFindAndModify: false,
useUnifiedTopology: true
})
}
private assets() {
this.app.use(express.static('public'))
this.app.use(express.static('views'))
}
private template() {
this.app.set('view engine', 'pug')
}
public listen() {
this.app.listen(this.port, () => {
console.log(`App listening on the http://localhost:${this.port}`)
})
}
}
export default App
src/server.ts
这是用于运行应用程序的文件。
import App from './app'
import * as bodyParser from 'body-parser'
import ShortenerController from './controllers/shortener/shortener.controller'
const app = new App({
port: 5000,
controllers: [
new ShortenerController()
],
middleWares: [
bodyParser.json(),
bodyParser.urlencoded({ extended: true }),
]
})
app.listen()
浏览
这个文件夹里会有视图文件。
views/home/home.pug
<!DOCTYPE html>
html(lang="en")
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
meta(http-equiv="X-UA-Compatible", content="ie=edge")
link(rel="stylesheet", href="css/bootstrap.css")
link(rel="stylesheet", href="css/app.css")
title TypeScript URL Shortener!
body
main(class="container")
div(class="jumbotron")
div(class="row")
div(class="col-md-12 align-self-center")
h1(class="text-center") URL Shortener
label(for="url") URL
div(class="input-group")
input.form-control(type="text", id="url", role="url", aria-label="Short URL")
div(class="input-group-append")
button(class="btn btn-md btn-danger", id="btn-short", role="button", aria-label="Short URL Button") Short URL
div(class="row")
div(class="col-md-12")
div(class="alert alert-danger invisible mt-3", id="url-alert" role="alert")
span(id="url-alert-text") URL shorthened
footer(class="footer")
div(class="container")
span(class="text-muted") TypeScript URL Shortener!
script(src="js/app.js")
MongoDB
要连接 MongoDB,我们需要一个 MongoDB 服务器。但我们无需安装新的 MongoDB 服务器,而是使用MongoDB Cloud。MongoDB Cloud提供免费套餐,无需付费。
创建账户后,您的集群将开始准备。您需要完成一些操作。首先,您需要创建一个数据库用户。
最后一步,你需要授予 IP 权限。在 MongoDB 云平台上,你必须这样做。
.env
此文件中将包含 MongoDB 信息;
MONGO_USER=YOUR MONGO USERNAME
MONGO_PASSWORD=YOUR MONGO PASSWORD
MONGO_PATH=YOUR MONGO DATABASE URL
就这些了。我们来运行应用程序吧 :)
npm run dev
截屏
结论
这次经历对我来说非常棒。我真的很喜欢 TypeScript 和 Express 与 MongoDB 的结合使用。
GitHub:https://github.com/aligoren/ts-url-shortener
文章来源:https://dev.to/itachiuchiha/building-url-shortener-with-mongodb-express-framework-and-typescript-4a71


