دليلك الشامل: بناء مشروع اختصار الروابط باستخدام Node.js و MongoDB

دقائق القراءة: 12
إذا كنت تتطلع إلى تعلم تقنيات جديدة، فما هي الطريقة الأفضل من تطبيقها عمليًا في مشروع متكامل؟ في هذا المقال، سنخوض غمار تجربة بناء تطبيق بسيط لاختصار الروابط، مستكشفين بذلك أساسيات استخدام MongoDB، Mongoose، و Node.js، بالإضافة إلى تقنيات أخرى ذات صلة. تطبيقات اختصار الروابط منتشرة في كل مكان، من الروابط التي تشاركها على Twitter إلى الخدمات الشهيرة مثل bit.ly. لكن هل تساءلت يومًا كيف يمكنك إنشاء خدمة اختصار روابط خاصة بك بسرعة؟ سنقدم لك دليلًا عمليًا خطوة بخطوة لبناء اختصار روابط يعتمد على MongoDB كحل لقاعدة البيانات الخلفية. سيمنحك هذا المشروع ثقة في معرفتك ويرسخ كل مفهوم تتعلمه. لنبدأ رحلتنا.

مقدمة المشروع وهيكله التقني

يهدف هذا المشروع إلى توفير تجربة تعليمية عملية ومتابعة تقدمك من خلال بناء تطبيق اختصار روابط. سنعتمد على مجموعة من التقنيات الأساسية لتطوير هذا التطبيق، وهي كالتالي:

  • Mongoose: كطبقة ORM (Object-Relational Mapping) للتعامل مع قاعدة البيانات.
  • MongoDB: كقاعدة بيانات خلفية لتخزين الروابط المختصرة والبيانات المتعلقة بها.
  • Node.js: كبيئة تشغيل للجهة الخلفية (backend) للتطبيق.
  • ملف JS مضمن بسيط: لتوفير واجهة المستخدم الأمامية (frontend).

سننجز هذا المشروع في سبع خطوات متسلسلة، تأخذك من البداية وحتى اكتمال التطبيق. دعنا نبدأ الآن في استكشاف هذه الخطوات.

الجزء الأول: إعداد خادم Express

الخطوة الأولى في مشروعنا هي إعداد خادم Node.js. سنستخدم إطار عمل Express.js نظرًا لسهولته ومرونته في بناء تطبيقات الويب. هذا الجزء يمثل تمرينًا بسيطًا نسبيًا، ويتطلب منا تجاوز تحديين رئيسيين:
تحديات إعداد خادم Express
يمكن أن يبدو الحل المقترح كالتالي:

 // Initialize express server on PORT 1337
const express = require('express')
const app = express()

app.get('/', (req, res) => {
  res.send('Hello World! - from codedamn')
})

app.get('/short', (req, res) => {
  res.send('Hello from short')
})

app.listen(process.env.PUBLIC_PORT, () => {
  console.log('Server started')
})

بكل بساطة، نقوم بإنشاء مسار GET آخر باستخدام الدالة app.get()، وهذا كافٍ لإنجاز المهمة المطلوبة في هذه المرحلة.

الجزء الثاني: تهيئة محرك العرض (View Engine)

بعد أن أصبحنا على دراية بعملية إعداد Express، حان الوقت لاستكشاف ملف القالب .ejs الذي سنستخدمه. يتيح لك محرك القوالب EJS تمرير المتغيرات من كود Node.js إلى ملفات HTML الخاصة بك، مما يمكنك من عرض البيانات أو تكرارها قبل إرسال الاستجابة النهائية إلى المتصفح. ألقِ نظرة سريعة على ملف views/index.ejs؛ ستجده مشابهًا لملف HTML عادي، لكن مع إمكانية استخدام المتغيرات بداخله. هذا هو ملف index.js الحالي لدينا:
ملف index.js الحالي مع إعداد محرك العرض
كما تلاحظ في ملف index.js، يوجد السطر app.set('view engine', 'ejs'). هذا السطر يخبر Express باستخدام ejs كمحرك القوالب الافتراضي. لاحظ أيضًا أننا نستخدم الدالة res.render() ونمرر اسم الملف فقط (مثل 'index') بدلاً من المسار الكامل، لأن Express سيبحث تلقائيًا داخل مجلد views عن قوالب .ejs المتاحة. نقوم بتمرير المتغيرات كوسيط ثانٍ للدالة res.render()، والتي يمكننا الوصول إليها لاحقًا داخل ملف EJS. سنستخدم هذا الملف في مراحل متقدمة من المشروع، ولكن دعنا الآن نتجاوز تحديًا سريعًا. لإكمال هذا التحدي، نحتاج فقط إلى تغيير الاسم من Mehul إلى أي اسم آخر. لتحقيق ذلك، قم أولاً بمعاينة ملف index.ejs ثم قم بتحديث الاسم إلى ما تفضله. إليك حل مقترح:

 const express = require('express')
const app = express()

app.set('view engine', 'ejs')

app.get('/', (req, res) => {
  res.render('index', { myVariable: 'My name is John!' })
})

app.listen(process.env.PUBLIC_PORT, () => {
  console.log('Server started')
})

الجزء الثالث: إعداد قاعدة بيانات MongoDB

بعد أن اكتسبنا فهمًا مبدئيًا لكل من الواجهة الأمامية والخلفية، دعنا ننتقل إلى إعداد قاعدة بيانات MongoDB. سنستخدم مكتبة Mongoose للاتصال بـ MongoDB. تعتبر Mongoose بمثابة ORM (Object-Relational Mapping) لـ MongoDB. ببساطة، MongoDB هي قاعدة بيانات مرنة للغاية وتسمح بإجراء جميع أنواع العمليات على أي نوع من البيانات. بينما تُعد هذه المرونة ميزة رائعة للبيانات غير المهيكلة، إلا أننا في معظم الأحيان نكون على دراية بهيكل البيانات المتوقع (مثل سجلات المستخدمين أو سجلات الدفع). لذلك، يمكننا تعريف مخطط (schema) لـ MongoDB باستخدام Mongoose. هذا يسهل علينا العديد من الوظائف؛ على سبيل المثال، بمجرد تحديد المخطط، يمكننا التأكد من أن التحقق من صحة البيانات وأي فحوصات ضرورية سيتم التعامل معها تلقائيًا بواسطة Mongoose. كما توفر لنا Mongoose مجموعة من الدوال المساعدة لجعل عملية التطوير أسهل. لنقم بإعدادها الآن.
لإكمال هذا الجزء، يجب علينا مراعاة النقاط التالية:

  • تم تثبيت حزمة Mongoose NPM مسبقًا. يمكنك استدعائها مباشرة باستخدام require.
  • الاتصال بعنوان URL الخاص بـ MongoDB وهو mongodb://localhost:27017/codedamn باستخدام الدالة mongoose.connect().

هذا هو ملف index.js الحالي لدينا:

 const express = require('express')
const app = express()
const mongoose = require('mongoose')

app.set('view engine', 'ejs')

app.get('/', (req, res) => {
  res.render('index')
})

app.post('/short', (req, res) => {
  const db = mongoose.connection.db
  // insert the record in 'test' collection
  res.json({ ok: 1 })
})

// Setup your mongodb connection here
// mongoose.connect(...)

// Wait for mongodb connection before server starts
app.listen(process.env.PUBLIC_PORT, () => {
  console.log('Server started')
})

دعنا نملأ الأماكن الفارغة بالتعليمات البرمجية المناسبة:

 const express = require('express')
const app = express()
const mongoose = require('mongoose')

app.set('view engine', 'ejs')

app.get('/', (req, res) => {
  res.render('index')
})

app.post('/short', (req, res) => {
  const db = mongoose.connection.db
  // insert the record in 'test' collection
  db.collection('test').insertOne({ testCompleted: 1 })
  res.json({ ok: 1 })
})

// Setup your mongodb connection here
mongoose.connect('mongodb://localhost/codedamn', { useNewUrlParser: true, useUnifiedTopology: true })

mongoose.connection.on('open', () => {
  // Wait for mongodb connection before server starts
  app.listen(process.env.PUBLIC_PORT, () => {
    console.log('Server started')
  })
})

لاحظ كيف نقوم ببدء تشغيل خادم HTTP فقط عندما يكون اتصالنا بـ MongoDB مفتوحًا. هذا أمر مهم لضمان عدم وصول المستخدمين إلى مسارات التطبيق قبل أن تكون قاعدة البيانات جاهزة للتعامل مع الطلبات. أخيرًا، نستخدم هنا الدالة db.collection() لإدراج سجل بسيط، ولكن سيكون لدينا طريقة أفضل قريبًا للتفاعل مع قاعدة البيانات باستخدام نماذج Mongoose.

الجزء الرابع: إعداد مخطط Mongoose (Schema)

بعد أن اكتسبنا خبرة عملية في تطبيق MongoDB في القسم السابق، دعنا الآن نحدد مخطط البيانات (schema) لتطبيق اختصار الروابط الخاص بنا. يتيح لنا مخطط Mongoose التفاعل مع مجموعات Mongo بطريقة مجردة ومنظمة. توفر مستندات Mongoose الغنية أيضًا دوال مساعدة مثل .save()، والتي تكفي لإجراء استعلام كامل لقاعدة البيانات لتحديث التغييرات في المستند الخاص بك. إليك كيف سيبدو مخططنا لاختصار الروابط:

 const mongoose = require('mongoose')
const shortId = require('shortid')

const shortUrlSchema = new mongoose.Schema({
  full: {
    type: String,
    required: true
  },
  short: {
    type: String,
    required: true,
    default: shortId.generate
  },
  clicks: {
    type: Number,
    required: true,
    default: 0
  }
})

module.exports = mongoose.model('ShortUrl', shortUrlSchema)

سنقوم بتخزين هذا الملف في المسار models/url.js. بمجرد الانتهاء من إعداد المخطط، يمكننا تجاوز هذا الجزء من التمرين. يتعين علينا القيام بأمرين رئيسيين:

  • إنشاء هذا النموذج (model) في ملف models/url.js. (لقد قمنا بذلك).
  • يجب أن يقوم طلب POST إلى المسار /short بإضافة شيء ما إلى قاعدة البيانات باستخدام هذا النموذج.

لتحقيق ذلك، يمكننا إنشاء سجل جديد باستخدام الكود التالي:

 app.post('/short', async (req, res) => {
  // insert the record using the model
  const record = new ShortURL({ full: 'test' })
  await record.save()
  res.json({ ok: 1 })
})

ستلاحظ أنه يمكننا حذف حقلي clicks و short لأن لديهما قيمًا افتراضية محددة مسبقًا في المخطط. هذا يعني أن Mongoose ستقوم بتعبئتهما تلقائيًا عند تنفيذ الاستعلام. يجب أن يبدو ملف index.js النهائي لدينا لتجاوز هذا التحدي كالتالي:

 const express = require('express')
const app = express()
const mongoose = require('mongoose')

// import the model here
const ShortURL = require('./models/url')

app.set('view engine', 'ejs')

app.get('/', (req, res) => {
  res.render('index', { myVariable: 'My name is John!' })
})

app.post('/short', async (req, res) => {
  // insert the record using the model
  const record = new ShortURL({ full: 'test' })
  await record.save()
  res.json({ ok: 1 })
})

// Setup your mongodb connection here
mongoose.connect('mongodb://localhost/codedamn')

mongoose.connection.on('open', () => {
  // Wait for mongodb connection before server starts
  app.listen(process.env.PUBLIC_PORT, () => {
    console.log('Server started')
  })
})

الجزء الخامس: ربط الواجهة الأمامية والخلفية وقاعدة البيانات MongoDB

الآن بعد أن أصبح لدينا فهم جيد للجزء الخلفي من التطبيق، دعنا نعود إلى الواجهة الأمامية ونقوم بإعداد صفحة الويب الخاصة بنا. هناك، يمكننا استخدام زر “Shrink” لإضافة بعض السجلات فعليًا إلى قاعدة البيانات. إذا نظرت داخل ملف views/index.ejs، ستجد أننا قمنا بالفعل بتمرير بيانات النموذج إلى مسار الواجهة الخلفية /short. لكن في الوقت الحالي، لا نقوم بالتقاط هذه البيانات. ستلاحظ وجود سطر جديد وهو app.use(express.urlencoded({ extended: false }))، والذي يسمح لنا بقراءة استجابة المستخدم من النموذج. في ملف index.ejs، يمكنك أن ترى أننا قمنا بتعيين name="fullUrl"، وهي الطريقة التي يمكننا من خلالها استقبال عنوان URL في الواجهة الخلفية. إليك ملف index.ejs الخاص بنا:

 <!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="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" />
  <title>codedamn URL Shortner Project</title>
</head>
<body>
  <div class="container">
    <h1>URL Shrinker</h1>
    <form action="/short" method="POST" class="my-4 form-inline">
      <label for="fullUrl" class="sr-only">URL</label>
      <input required placeholder="URL" type="url" name="fullUrl" id="fullUrl" class="form-control col mr-2" />
      <button class="btn btn-success" type="submit">Shrink This!</button>
    </form>
    <div style="overflow-x: auto; max-width: 100%;">
    <table class="table table-striped table-responsive">
      <thead>
        <tr>
          <th>Full URL</th>
          <th>Short URL</th>
          <th>Clicks</th>
        </tr>
      </thead>
      <tbody>
        <% shortUrls.forEach(shortUrl => { %>
          <tr>
            <td><a href="<%= shortUrl.full %>"><%= shortUrl.full %></a></td>
            <td><a href="<%= shortUrl.short %>"><%= shortUrl.short %></a></td>
            <td><%= shortUrl.clicks %></td>
          </tr>
        <% }) %>
      </tbody>
    </table>
    </div>
  </div>
</body>
</html>

هذا تحدٍ بسيط، لأننا نحتاج فقط إلى وضع هذا الكود لإكماله:

 app.use(express.urlencoded({ extended: false }))

app.post('/short', async (req, res) => {
  // Grab the fullUrl parameter from the req.body
  const fullUrl = req.body.fullUrl
  console.log('URL requested: ', fullUrl)
  // insert and wait for the record to be inserted using the model
  const record = new ShortURL({ full: fullUrl })
  await record.save()
  res.redirect('/')
})

أولاً وقبل كل شيء، نقوم بالتقاط عنوان URL المرسل عبر HTML باستخدام req.body.fullUrl. لتمكين ذلك، نستخدم أيضًا app.use(express.urlencoded({ extended: false })) الذي يسمح لنا بالحصول على بيانات النموذج. ثم نقوم بإنشاء سجلنا وحفظه تمامًا كما فعلنا في المرة السابقة. أخيرًا، نقوم بإعادة توجيه المستخدم إلى الصفحة الرئيسية حتى يتمكن من رؤية الروابط الجديدة.
نصيحة: يمكنك جعل هذا التطبيق أكثر إثارة للاهتمام عن طريق إجراء طلب Ajax إلى واجهة برمجة التطبيقات الخلفية بدلاً من إرسال النموذج التقليدي. لكننا سنكتفي بهذا القدر هنا حيث يركز المشروع بشكل أكبر على إعداد MongoDB و Node.js بدلاً من تعقيدات JavaScript.

الجزء السادس: عرض الروابط المختصرة في الواجهة الأمامية

الآن بعد أن أصبحنا نخزن الروابط المختصرة في قاعدة بيانات MongoDB، دعنا ننتقل إلى عرضها في الواجهة الأمامية أيضًا. هل تتذكر المتغيرات التي مررناها إلى قالب EJS سابقًا؟ حان الوقت لاستخدامها. تم إعداد حلقة القالب الخاصة بـ EJS لك بالفعل في ملف index.ejs (يمكنك رؤية تلك الحلقة أعلاه). ومع ذلك، يتعين علينا كتابة استعلام Mongoose لاستخراج البيانات في هذا القسم. إذا نظرنا إلى القالب، سنجد في ملف index.js الكود التالي:

 app.get('/', (req, res) => {
  const allData = []
  // write a mongoose query to get all URLs from here
  res.render('index', { shortUrls: allData })
})

لدينا بالفعل نموذج (model) محدد يمكننا من خلاله استعلام البيانات من Mongoose. دعنا نستخدمه للحصول على كل ما نحتاجه. إليك ملف الحل الخاص بنا:

 const express = require('express')
const app = express()
const mongoose = require('mongoose')

// import the model here
const ShortURL = require('./models/url')

app.set('view engine', 'ejs')
app.use(express.urlencoded({ extended: false }))

app.get('/', async (req, res) => {
  const allData = await ShortURL.find()
  res.render('index', { shortUrls: allData })
})

app.post('/short', async (req, res) => {
  // Grab the fullUrl parameter from the req.body
  const fullUrl = req.body.fullUrl
  console.log('URL requested: ', fullUrl)
  // insert and wait for the record to be inserted using the model
  const record = new ShortURL({ full: fullUrl })
  await record.save()
  res.redirect('/')
})

// Setup your mongodb connection here
mongoose.connect('mongodb://localhost/codedamn', { useNewUrlParser: true, useUnifiedTopology: true })

mongoose.connection.on('open', async () => {
  // Wait for mongodb connection before server starts
  // Just 2 URLs for testing purpose
  await ShortURL.create({ full: 'http://google.com' })
  await ShortURL.create({ full: 'http://codedamn.com' })
  app.listen(process.env.PUBLIC_PORT, () => {
    console.log('Server started')
  })
})

كما ترى، كان الأمر سهلاً بمجرد استخدام await ShortURL.find() في المتغير allData. الجزء التالي هو المكان الذي تصبح فيه الأمور أكثر تعقيدًا بعض الشيء.

الجزء السابع: تفعيل وظيفة إعادة التوجيه

لقد أوشكنا على الانتهاء! أصبح لدينا الآن عنوان URL الكامل والـ URL المختصر مخزنين في قاعدة البيانات، ونعرضهما أيضًا في الواجهة الأمامية. لكنك ستلاحظ أن إعادة التوجيه لا تعمل حاليًا ونحصل على خطأ من Express. دعنا نصلح ذلك. يمكنك أن ترى في ملف index.js أنه تمت إضافة مسار ديناميكي جديد في النهاية يتعامل مع عمليات إعادة التوجيه هذه:

 app.get('/:shortid', async (req, res) => {
  // grab the :shortid param
  const shortid = ''
  // perform the mongoose call to find the long URL
  // if null, set status to 404 (res.sendStatus(404))
  // if not null, increment the click count in database
  // redirect the user to original link
})

تبدو التحديات التي تواجهنا في هذا الجزء كالتالي:
تحديات تفعيل إعادة التوجيه في تطبيق اختصار الروابط
تمام. أولاً وقبل كل شيء، يجب علينا استخراج عنوان URL الكامل عندما نزور URL مختصرًا. إليك كيفية القيام بذلك:

 app.get('/:shortid', async (req, res) => {
  // grab the :shortid param
  const shortid = req.params.shortid
  // perform the mongoose call to find the long URL
  const rec = await ShortURL.findOne({ short: shortid })
  // ...
})

الآن، إذا وجدنا أن النتيجة فارغة (null)، فسنرسل حالة 404 (غير موجود):

 app.get('/:shortid', async (req, res) => {
  // grab the :shortid param
  const shortid = req.params.shortid
  // perform the mongoose call to find the long URL
  const rec = await ShortURL.findOne({ short: shortid })
  // if null, set status to 404 (res.sendStatus(404))
  if (!rec) return res.sendStatus(404)
  res.sendStatus(200)
})

هذا ينهي تحدينا الأول بنجاح. بعد ذلك، إذا كان لدينا رابط فعلي، فلنعيد توجيه المستخدم ونزيد عدد النقرات في قاعدة البيانات أيضًا:

 app.get('/:shortid', async (req, res) => {
  // grab the :shortid param
  const shortid = req.params.shortid
  // perform the mongoose call to find the long URL
  const rec = await ShortURL.findOne({ short: shortid })
  // if null, set status to 404 (res.sendStatus(404))
  if (!rec) return res.sendStatus(404)
  // if not null, increment the click count in database
  rec.clicks++
  await rec.save()
  // redirect the user to original link
  res.redirect(rec.full)
})

بهذه الطريقة، يمكننا زيادة عدد النقرات وتخزين النتيجة في قاعدة البيانات مرة أخرى. وهذا يجب أن ينهي جميع تحدياتنا.

الخلاصة التقنية

تهانينا! لقد نجحت للتو في بناء تطبيق كامل لاختصار الروابط يعمل بكفاءة باستخدام Express و Node.js و MongoDB. هذا المشروع العملي يمثل خطوة مهمة في رحلتك التعليمية، حيث يدمج بين مفاهيم تطوير الواجهة الخلفية وإدارة قواعد البيانات. لقد استعرضنا كيفية إعداد خادم Express، وتهيئة محرك العرض EJS، والاتصال بـ MongoDB عبر Mongoose، وتحديد مخطط البيانات، وربط الواجهة الأمامية بالخلفية، وأخيرًا تفعيل وظيفة إعادة التوجيه الديناميكية مع تتبع النقرات. هذه المهارات أساسية لأي مطور ويب يطمح لبناء تطبيقات قوية وفعالة.
الكود المصدري النهائي للمشروع متاح على GitHub للمراجعة والتعلم الإضافي. إذا كان لديك أي ملاحظات حول هذا المقال، فلا تتردد في التواصل.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *