بناء تطبيق Node.js على أندرويد: الجزء الثاني – Express و NeDB

دقائق القراءة: 12

مقدمة

في الجزء الأول من هذه السلسلة، استكشفنا كيفية استخدام Termux، وهو محاكي طرفية وبيئة Linux لنظام أندرويد. كما قمنا بتحرير الملفات باستخدام Vim ورأينا كيفية تشغيل Node.js. الآن، سنتعمق في بناء تطبيق Node.js صغير باستخدام إطار عمل Express وسنستخدم NeDB كقاعدة بيانات.

القصة ومن يمكنه الاستفادة

عندما اكتشفت إمكانية بناء تطبيق Node.js كامل مع قاعدة بيانات شبيهة بـMongo على جهاز لوحي يعمل بنظام أندرويد، شعرت بحماس كبير. قررت تجربتها ومشاركة تجربتي. اتضح أنه بمجرد تشغيل Termux على أندرويد وتثبيت Node.js، فإن حقيقة أننا نعمل على أندرويد بدلاً من Linux لا تحدث فرقًا كبيرًا. في الواقع، جميع الإعدادات الخاصة بـTermux تم إنجازها في الجزء الأول، ويمكنك متابعة البرمجة على جهازك المفضل أو حاسوبك أو بيئة التطوير السحابية (cloud IDE).

هذا يعني أيضًا، بغض النظر عن استبدال Mongo بـNeDB، أن هذا المقال يشبه المقدمة المعتادة لبناء واجهة برمجة تطبيقات RESTful API، وهو موجه بشكل أساسي للمبتدئين في Node.js وExpress وMongo/NeDB.

ما الذي سنقوم به؟

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

  • إرسال هدف جديد.
  • حذف هدف.
  • تسجيل نجاح لهدف.
  • تسجيل فشل لهدف.

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

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

  1. إعداد الخادم باستخدام Express.
  2. وصف بعض قصص المستخدم (user stories).
  3. إعداد NeDB.
  4. بناء واجهة برمجة تطبيقات RESTful API.

المتطلبات المسبقة

سنبدأ من حيث توقفنا في الجزء الأول. وبالتالي، فإن المتطلب الوحيد هو أن يكون Node.js مثبتًا.

1. إعداد الخادم باستخدام Express

Express هو إطار عمل ويب لـNode.js يساعد في بناء تطبيقات Node.js. إذا واجهت صعوبة في فهم ما يضيفه Express إلى Node.js، أنصحك بالاطلاع على مقال إيفان هان بعنوان Understanding Express.

لنبدأ مشروعًا جديدًا:

$ mkdir goals-tracker && cd goals-tracker
$ npm init
$ touch server.js

ثم قم بتثبيت Express:

npm install express --save

سنستخدم Express لتعريف routes (المسارات)، أي لتحديد نقاط نهاية التطبيق (URIs) وإعداد كيفية استجابة التطبيق لطلبات العميل.

افتح الملف server.js وانسخ/اكتب الشيفرة التالية:

// server.js

// DEPENDENCIES AND SETUP
// ===============================================
var express = require('express'),
    app = express(),
    port = Number(process.env.PORT || 8080);

// ROUTES
// ===============================================

// Define the home page route.
app.get('/', function (req, res) {
    res.send('Our first route is working.:)');
});

// START THE SERVER
// ===============================================
app.listen(port, function () {
    console.log('Listening on port ' + port);
});

مع هذا الإعداد، يمكنك بدء التطبيق:

$ node server.js

يجب أن يطبع هذا إلى الطرفية رقم المنفذ الذي يستمع عليه الخادم. إذا زرت http://localhost:8080 في المتصفح (بافتراض أن 8080 هو الرقم الذي طُبع إلى الطرفية)، يجب أن ترى على الصفحة:

Our first route is working. :)

بعض التوضيحات

الرمز '/' في app.get( … ) يحدد المسار الذي نريد ربط سلوك معين من الخادم به. استخدام '/' يشير إلى URI الأساسي، وفي حالتنا: http://localhost:8080. لاحظ أننا سنحصل على نفس النتيجة في نافذة المتصفح إذا استخدمنا app.get('/goals', …) بدلاً من ذلك وقمنا بزيارة http://localhost:8080/goals.

الوسيط الثاني في app.get( … ) هو دالة رد نداء (callback function) تمكننا من تحديد ما يجب أن يفعله الخادم عند زيارة المسار المعطى كوسيط أول. في هذه الدالة:

  • req يرمز إلى الطلب (request): وهي المعلومات التي يتلقاها الخادم من العميل (على سبيل المثال، قد تأتي هذه من شخص يستخدم الجزء الأمامي (front-end) من الموقع/التطبيق).
  • res يرمز إلى الاستجابة (response): وهي المعلومات التي يرسلها الخادم مرة أخرى إلى المستخدم. يمكن أن تكون هذه صفحة ويب أو بعض البيانات الأخرى مثل صورة، أو JSON، أو XML.

Nodemon

في الأجزاء التالية من هذا الدرس، سنجري العديد من التغييرات على الملف server.js. لتجنب إعادة تشغيل الخادم يدويًا في كل مرة لرؤية النتيجة، يمكننا تثبيت nodemon. Nodemon هي أداة مساعدة ستراقب التغييرات في شيفرتك وتعيد تشغيل الخادم تلقائيًا. سنقوم بتثبيتها كاعتمادية للتطوير فقط باستخدام العلامة save-dev:

npm install nodemon --save-dev

الآن يمكنك إعادة تشغيل الخادم باستخدام أمر nodemon بدلاً من node:

nodemon server.js

2. قصص المستخدم

قبل الانتقال إلى الجزء المتعلق بـNeDB، دعنا نتوقف لحظة للتفكير في منطق العمل (business logic). لكي نرى ما نحتاج إلى تنفيذه، نبدأ بتعريف بعض قصص المستخدم (user stories). قصة المستخدم هي تعريف عالي المستوى جدًا للمتطلب. قصص المستخدم مفيدة لمناقشة المنتج بمصطلحات غير تقنية مع العميل، لتقدير مقدار الوقت والجهد الذي سيستغرقه تنفيذ ميزة ما، لتوجيه التطوير العام للتطبيق، وللقيام بالتطوير القائم على الاختبار (Test Driven Development).

إليك قصص المستخدم الأربع التي سنستخدمها:

  • كمستخدم، يمكنني حفظ هدف جديد مع تاريخ إنشائه.
  • كمستخدم، يمكنني الوصول إلى جميع الأهداف التي تم حفظها.
  • كمستخدم، يمكنني الوصول إلى المعلومات الكاملة حول هدف.
  • كمستخدم، يمكنني حذف هدف.

في حالتنا، تتطابق القصص بشكل مباشر مع عمليات CRUD الأربع التي سنتحدث عنها في الجزء الرابع.

3. استخدام NeDB

حقيقة أن NeDB سهل التثبيت، موثق جيدًا، ويستخدم واجهة برمجة تطبيقات MongoDB تجعله مثاليًا للبدء في تطوير تطبيقات Node.js على أندرويد. توجد حتى أداة لمساعدتك على التحول إلى MongoDB لاحقًا إذا لزم الأمر (لم أجربها بعد).

لذلك، دعنا نضيف NeDB إلى المشروع:

$ npm install nedb --save

وأضف إلى server.js بضعة أسطر لإعداد قاعدة البيانات والتأكد من قدرتنا على الحفظ فيها:

// server.js

// DEPENDENCIES AND SETUP
// ===============================================
var express = require('express'),
    app = express(),
    port = Number(process.env.PORT || 8080);

// DATABASE
// ===============================================

// Setup the database.
var Datastore = require('nedb');
var db = new Datastore({
    filename: 'goals.db', // provide a path to the database file
    autoload: true,     // automatically load the database
    timestampData: true // automatically add and manage the fields createdAt and updatedAt
});

// Let us check that we can save to the database.
// Define a goal.
var goal = {
    description: 'Do 10 minutes meditation every day',
};

// Save this goal to the database.
db.insert(goal, function (err, newGoal) {
    if (err) console.log(err);
    console.log(newGoal);
});

// ROUTES
// ===============================================

// Define the home page route.
app.get('/', function (req, res) {
    res.send('Our first route is working. :)');
});

// START THE SERVER
// ===============================================
app.listen(port, function () {
    console.log('Listening on port ' + port);
});

يشير Datastore إلى ما يُسمى collection في Mongo. يمكننا إنشاء عدة datastores إذا احتجنا إلى عدة collections. كما هو موضح في وثائق NeDB، سيتم حفظ كل collection في ملف منفصل. هنا اخترنا تخزين collection الأهداف في ملف يسمى goals.db.

التحقق مما إذا كان يعمل

إذا تم بدء تشغيل الخادم سابقًا باستخدام nodemon، فيجب أن يكون قد تم تحديثه بعد حفظ التغييرات في server.js. هذا يعني أن db.insert(…) يجب أن يكون قد تم تشغيله وأن الهدف يجب أن يكون قد تم تسجيله في الطرفية:

$ nodemon server.js
[nodemon] 1.9.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node server.js`
Listening on port 8080
[nodemon] restarting due to changes...
[nodemon] starting `node server.js`
Listening on port 8080
{
    description: 'Do 10 minutes meditation every day',
    successes: [],
    failures: [],
    _id: 'ScfixKjsOqz9xBo5',
    createdAt: Fri Mar 18 2016 22:10:58 GMT+0000 (UTC),
    updatedAt: Fri Mar 18 2016 22:10:58 GMT+0000 (UTC)
}

يجب أيضًا أن يكون قد تم إنشاء ملف جديد يسمى goals.db.

$ ls
goals.db
node_modules/
package.json
server.js

ويجب أن يحتوي على الهدف الذي تم حفظه للتو.

$ less goals.db
{ "description": "Do 10 minutes meditation every day", "_id": "ScfixKjsOqz9xBo5", "createdAt":{"$date":1458339058282}, "updatedAt":{"$date":1458339058282}}

لاحظ أن الحقول _id وcreatedAt وupdatedAt قد تم ملؤها تلقائيًا بواسطة NeDB لأننا قمنا بإعداد Datastore بخيار timestampData مضبوطًا على true.

4. بناء واجهة برمجة تطبيقات RESTful

بعد ذلك، دعنا نبني واجهة برمجة تطبيقات RESTful API للتطبيق. باختصار، هذا يعني أننا نريد استخدام أفعال HTTP وURIs للسماح للعميل بإجراء عمليات CRUD (الإنشاء Create، القراءة Read، التحديث Update، والحذف Delete). عادةً ما تقوم هذه العمليات أيضًا بإرسال البيانات مرة أخرى إلى العميل.

بمصطلحات CRUD يمكننا:

  • إنشاء البيانات باستخدام POST.
  • قراءة البيانات باستخدام GET.
  • تحديث البيانات باستخدام PUT أو PATCH.
  • حذف البيانات باستخدام DELETE.

أفعال HTTP التي سنستخدمها في هذا المنشور هي POST وGET وDELETE.

واجهة برمجة التطبيقات الخاصة بنا

فيما يلي وصف للمسارات التي سنقوم بإعدادها، وكيفية الوصول إليها (أي باستخدام أي فعل HTTP)، وما الذي يتيحه كل منها:

  • لإنشاء هدف جديد: مسار /goals باستخدام فعل POST.
  • للحصول على جميع الأهداف: مسار /goals باستخدام فعل GET.
  • للحصول على هدف معين بواسطة معرفه: مسار /goals/:id باستخدام فعل GET.
  • لحذف هدف بواسطة معرفه: مسار /goals/:id باستخدام فعل DELETE.

إذا كنت ترغب في معرفة المزيد عن واجهات برمجة تطبيقات RESTful APIs، يمكنك الاطلاع على مقال Designing a RESTful Web API لماثياس هانسن أو Using HTTP Methods for RESTful Services.

اختبار واجهة برمجة التطبيقات

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

دعنا نرى مثالاً أولياً لكيفية استخدام curl. تأكد من أن الخادم يعمل، افتح طرفية أخرى (في Termux، اسحب إلى اليمين من الحافة اليسرى وانقر على new session) واكتب:

$ curl -X GET "http://localhost:8080"

يجب أن يطبع هذا إلى الطرفية ما حصلنا عليه في نافذة المتصفح في وقت سابق، وهو:

Our first route is working. :)

سنضيف الآن شيفرة إلى server.js تدريجيًا. إذا كنت تفضل رؤية الصورة الكبيرة أولاً، يمكنك التوجه إلى ملف server.js النهائي.

Body-parser

للتعامل مع الطلبات التي يتلقاها الخادم، سنقوم بتثبيت body-parser. يقوم بمعالجة الطلبات الواردة ويسهل علينا الوصول إلى الأجزاء ذات الصلة.

npm install body-parser --save

أضف شيفرة إعداد body-parser إلى الجزء العلوي من server.js (على سبيل المثال، مباشرة بعد إعداد رقم المنفذ):

var bodyParser = require('body-parser'), // Middleware to read POST data

// Set up body-parser.
// To parse JSON:
app.use(bodyParser.json());
// To parse form data:
app.use(bodyParser.urlencoded({ extended: true }));

الحصول على جميع الأهداف

// GET all goals.
// (Accessed at GET http://localhost:8080/goals)
app.get('/goals', function (req, res) {
    db.find({}).sort({ updatedAt: -1 }).exec(function (err, goals) {
        if (err) res.send(err);
        res.json(goals);
    });
});

إذا تلقى الخادم طلب GET على /goals، فسيتم تنفيذ دالة رد النداء وسيتم الاستعلام عن قاعدة البيانات باستخدام db.find({}). هنا الكائن الذي تم تمريره إلى find() فارغ. هذا يعني أنه لا يوجد قيد محدد لما نبحث عنه ويجب إرجاع جميع الكائنات في قاعدة البيانات. لاحظ أيضًا أنه لم يتم تحديد دالة رد نداء لـfind(). وبالتالي يتم إرجاع كائن Cursor، والذي يمكن تعديله أولاً باستخدام sort أو skip أو limit قبل أن نستخدم exec(callback) لإنهاء الاستعلام. هنا نستخدم sort لإرجاع الأهداف مع الأحدث تحديثًا في الأعلى (أي تلك التي تحتوي على تاريخ تحديث أخير ‘أكبر’). إذا سار كل شيء على ما يرام، يتم إرسال نتيجة الاستعلام (في حالتنا، مصفوفة من الأهداف) مرة أخرى إلى العميل بتنسيق JSON. في حال حدوث خطأ، يتم إرسال رسالة الخطأ مرة أخرى إلى العميل بدلاً من ذلك.

دعنا نختبر ما إذا كان يعمل:

$ curl -X GET "http://localhost:8080/goals/"

يجب أن يطبع هذا إلى الطرفية مصفوفة تحتوي على الهدف الذي حفظناه في قاعدة البيانات سابقًا.

إنشاء هدف

// POST a new goal.
// (Accessed at POST http://localhost:8080/goals)
app.post('/goals', function (req, res) {
    var goal = {
        description: req.body.description,
    };
    db.insert(goal, function (err, goal) {
        if (err) res.send(err);
        res.json(goal);
    });
});

يحتوي req.body على أزواج مفتاح-قيمة للبيانات التي تم إرسالها في جسم الطلب (request body). بشكل افتراضي، يكون غير معرف ويتم ملؤه بواسطة وسيط body-parser. في حالتنا، يجب أن يحتوي الطلب على زوج مفتاح-قيمة يكون مفتاحه 'description' وبالتالي يتم استرداد قيمته باستخدام req.body.description. لذا، أولاً، يتم بناء الهدف الذي نريد إدخاله في قاعدة البيانات من الطلب باستخدام req.body.description. ثم يمكن إدخاله في قاعدة البيانات، وإذا لم يكن هناك خطأ، يتم إرسال الاستجابة مرة أخرى إلى الخادم بتنسيق JSON.

الآن دعنا نحاول إرسال هدف جديد باستخدام curl:

$ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "description=Read about functional programming every day" "http://localhost:8080/goals/"

يجب أن يطبع هذا التمثيل بتنسيق JSON للهدف الذي تم إرساله مرة أخرى إلى العميل. نحن نرسل البيانات بتنسيق x-www-form-urlencoded. هذا يرسل البيانات كسلاسل استعلام يتم تحليلها بواسطة body-parser.

الحصول على هدف باستخدام معرفه

// GET a goal.
// (Accessed at GET http://localhost:8080/goals/goal_id)
app.get('/goals/:id', function (req, res) {
    var goal_id = req.params.id;
    db.findOne({ _id: goal_id }, {}, function (err, goal) {
        if (err) res.send(err);
        res.json(goal);
    });
});

إن req.params هو كائن يحتوي على خصائص معينة مرتبطة بـ"parameters" المسار. هنا، يمكننا من الوصول إلى قيمة معرف الهدف (goal's id)، والذي يُفترض أن يأتي بعد /goals/ في عنوان URL ضمن الطلب. لكي يعمل هذا، يجب أن نستخدم نقطتين (:) في URI قبل الخاصية التي نريد الوصول إليها باستخدام req.params. بخلاف حقيقة أننا نستخدم findOne(…) بدلاً من find(…)، لا يوجد شيء جديد هنا. لذا دعنا نختبره. لهذا، يمكنك التحقق مما تم طباعته في الطرفية بعد أن حفظنا هدفًا جديدًا باستخدام POST واستخدام قيمة _id الخاصة به. إليك أمري مع المعرف الذي حصلت عليه:

$ curl -X GET "http://localhost:8080/goals/JJtcFVQnoAxW7KXc"

يجب أن يطبع هذا إلى الطرفية الهدف بالمعرف المحدد.

حذف هدف باستخدام معرفه

// DELETE a goal.
// (Accessed at DELETE http://localhost:8080/goals/goal_id)
app.delete('/goals/:id', function (req, res) {
    var goal_id = req.params.id;
    db.remove({ _id: goal_id }, {}, function (err, goal) {
        if (err) res.send(err);
        res.sendStatus(200);
    });
});

نستخدم remove(…) لحذف هدف من قاعدة البيانات. إذا كان الحذف ناجحًا، يتم إرسال الاستجابة مع رمز حالة HTTP وهو 200 (200 يعني أن الحذف كان ناجحًا).

خاتمة

لقد قمنا بإعداد خادم باستخدام Express وNeDB، وبناء واجهة برمجة تطبيقات RESTful API. ما زلنا نفتقد إلى واجهة أمامية (front-end) وبعض الربط. يمكن أن تأخذنا هذه الخطوة التالية إلى العديد من الطرق المختلفة:

  • هل سنختار محرك قوالب (template engine)، وإذا كان الأمر كذلك، فأيها؟
  • Bootstrap أو إطار عمل مشابه؟
  • Angular، React، Aurelia؟

القائمة تطول وتطول. إذا كنت ترغب في إلقاء نظرة على تطبيق بسيط لواجهة أمامية – وربما تجربتها في متصفحك – يمكنك رؤية الشيفرة البرمجية لحل ممكن قمت بتنفيذه باستخدام Handlebars وMaterial Design Lite وfetch API من خلال زيارة مستودعها على GitHub أو استنساخها:

$ git clone --branch rest-and-view [رابط المستودع] --depth 1

متابعة التطوير

لا يزال الجزء الخلفي (back-end) الذي بنيناه يثير سؤالاً: كيف يجب تقسيم الشيفرة إلى ملفات ومجلدات مختلفة لتحقيق قابلية أفضل للنمطية والصيانة (modularity and maintainability)؟ إذا كنت مهتمًا، فقد قمت أيضًا بكتابة إصدار آخر من التطبيق يستخدم بنية مجلدات Model-View-Controller. لا تتردد في إلقاء نظرة:

$ git clone [رابط المستودع]

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

في هذا الجزء الثاني، نجحنا في بناء نواة تطبيق Node.js على بيئة أندرويد باستخدام Termux. لقد قمنا بإعداد خادم ويب قوي ومرن بواسطة Express، ودمجنا قاعدة بيانات NoSQL خفيفة الوزن NeDB التي تحاكي واجهة MongoDB API، مما يجعلها مثالية للبيئات المحدودة. الأهم من ذلك، قمنا بتصميم وتنفيذ واجهة برمجة تطبيقات RESTful API أساسية تدعم عمليات CRUD (الإنشاء، القراءة، الحذف) باستخدام أفعال HTTP المناسبة. هذه الخطوات تمثل أساسًا متينًا لأي تطبيق ويب، وتوضح كيف يمكن استغلال إمكانيات Node.js بالكامل حتى على الأجهزة المحمولة.

اترك تعليقاً

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