发布于 2026-01-06 4 阅读
0

使用 MongoDB、Express 框架和 TypeScript 构建 URL 缩短器

使用 MongoDB、Express 框架和 TypeScript 构建 URL 缩短器

这篇文章最初发表在我的博客上

大家好,我在上一篇文章中讨论了 Express 框架和 TypeScript。在本文中,我将沿用这种结构。

所以,我不会谈论我们将采用什么样的结构。

开始之前

在开始之前,我们将使用 MongoDB 来进行这个项目,为了获取环境变量值,我们将使用 dotenv 包。

nodemonNick Taylor向我推荐的。使用 nodemon,您无需手动停止和启动应用程序,它会自动为您完成这些操作。

mongoose:用于连接 MongoDB 的驱动程序。

dotenv:一个用于获取环境变量值的软件包。

安装软件包

npm i typescript nodemon express mongoose pug ts-node dotenv @types/node @types/mongoose @types/express
Enter fullscreen mode Exit fullscreen mode

让我们编辑package.json文件中的scripts部分。

"scripts": {
  "dev": "nodemon src/server.ts",
  "start": "ts-node dist/server.js",
  "build": "tsc -p ."
}
Enter fullscreen mode Exit fullscreen mode

tsconfig.json

{
    "compilerOptions": {
        "sourceMap": true,
        "target": "es6",
        "module": "commonjs",
        "outDir": "./dist",
        "baseUrl": "./src"
    },
    "include": [
        "src/**/*.ts"
    ],
    "exclude": [
        "node_modules"
    ]
}
Enter fullscreen mode Exit fullscreen mode

让我们创建一个项目结构

民众

CSS

在这个文件夹中,我们将有两个 CSS 文件,分别名为bootstrap.cssapp.css。bootstrap.css文件将使用 Bootstrap 4.x 版本,而 app.css 文件将用于自定义样式。

app.css

.right {
    float: inline-end;
}
Enter fullscreen mode Exit fullscreen mode
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')
        }

    }

})
Enter fullscreen mode Exit fullscreen mode

来源

控制器

这个文件夹里将包含控制器及其模型和接口文件。

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
Enter fullscreen mode Exit fullscreen mode
controllers/shortener.interface.ts

在这个接口中,我们使用名为 ISHortener 的接口。它有两个可选参数。

interface IShortener {
    longUrl?: string,
    shortUrl?: string
}

export default IShortener
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

接口

在这个文件夹中,我们只会有一个接口文件,名为IControllerBase

interfaces/IControllerBase.interface.ts
interface IControllerBase {
    initRoutes(): any
}

export default IControllerBase
Enter fullscreen mode Exit fullscreen mode

中间件

这里什么都没有,我们创建这个文件夹是为了以防您需要中间件。

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

浏览

这个文件夹里会有视图文件。

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")
Enter fullscreen mode Exit fullscreen mode

MongoDB

要连接 MongoDB,我们需要一个 MongoDB 服务器。但我们无需安装新的 MongoDB 服务器,而是使用MongoDB Cloud。MongoDB Cloud提供免费套餐,无需付费。

创建账户后,您的集群将开始准备。您需要完成一些操作。首先,您需要创建一个数据库用户。

MongoDB 管理员

最后一步,你需要授予 IP 权限。在 MongoDB 云平台上,你必须这样做。

MongoDB 网络

.env

此文件中将包含 MongoDB 信息;

MONGO_USER=YOUR MONGO USERNAME
MONGO_PASSWORD=YOUR MONGO PASSWORD
MONGO_PATH=YOUR MONGO DATABASE URL
Enter fullscreen mode Exit fullscreen mode

就这些了。我们来运行应用程序吧 :)

npm run dev
Enter fullscreen mode Exit fullscreen mode

截屏

URL缩短器截图

结论

这次经历对我来说非常棒。我真的很喜欢 TypeScript 和 Express 与 MongoDB 的结合使用。

GitHubhttps://github.com/aligoren/ts-url-shortener

文章来源:https://dev.to/itachiuchiha/building-url-shortener-with-mongodb-express-framework-and-typescript-4a71