استخدام Routes Express.js لمعالجة أخطاء promise-based

Routes Express.js

يبدو أن شعار Express.js صحيحًا: إنه “إطار عمل ويب بسيط وسريع وغير معلن لـ Node.js“. إنه غير معلن أنه على الرغم من القيام بأفضل ممارسات جافا سكريبت الحالية التي تنص على استخدام الوعود، فإن Express.js لا يدعم معالجات المسارات المستندة إلى برامیس افتراضيًا.

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

البنية النموذجية لمسارات Express.js

لنبدأ بتطبيق البرنامج التعليمي Express.js الذي يحتوي على بعض المسارات لنموذج المستخدم.

في المشاريع الحقيقية، نقوم بتخزين البيانات ذات الصلة في بعض قواعد البيانات مثل MongoDB. ولكن للأغراض الخاصة بنا، فإن تفاصيل تخزين البيانات غير مهمة، لذلك سنستهزئ بها من أجل البساطة. ما لن نقوم بتبسيطه هو هيكل مشروع جي ، وهو مفتاح نصف نجاح أي مشروع.

يمكن أن ينتج Yeoman هياكل عظمية أفضل بكثير للمشروع بشكل عام، ولكن لما نحتاج إليه، سننشئ ببساطة هيكلًا عظميًا للمشروع باستخدام مولد سريع ونزيل الأجزاء غير الضرورية، حتى نحصل على هذا:

bin
  start.js
node_modules
routes
  users.js
services
  userService.js
app.js
package-lock.json
package.json

لقد قمنا باختصار سطور الملفات المتبقية التي لا تتعلق بأهدافنا.

فيما يلي ملف تطبيق Express.js الرئيسي ./app.js :

const createError  = require('http-errors');
const express = require('express');
const cookieParser = require('cookie-parser');
const usersRouter = require('./routes/users');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use('/users', usersRouter);
app.use(function(req, res, next) {
  next(createError(404));
});
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.send(err);
});

module.exports = app;

نقوم هنا بإنشاء تطبيق Express.js وإضافة بعض البرامج الوسيطة الأساسية لدعم استخدام JSON وترميز عنوان URL وتحليل ملفات تعريف الارتباط. نضيف بعد ذلك userRouter لـ users/ . أخيرًا ، نحدد ما يجب فعله في حالة عدم العثور على مسار، وكيفية التعامل مع الأخطاء ، والتي سنقوم بتغييرها لاحقًا.

البرنامج النصي لبدء الخادم نفسه هو bin/start.js/

const app = require('../app');
const http = require('http');

const port = process.env.PORT || '3000';

const server = http.createServer(app);
server.listen(port);

لدينا package.json/ أيضًا مجردة:

{
  "name": "express-promises-example",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/start.js"
  },
  "dependencies": {
    "cookie-parser": "~1.4.4",
    "express": "~4.16.1",
    "http-errors": "~1.6.3"
  }
}

لنستخدم تنفيذاً نموذجياً لجهاز توجيه المستخدم في routes/users.js:

const router = express.Router();

const userService = require('../services/userService');

router.get('/', function(req, res) {
  userService.getAll()
    .then(result => res.status(200).send(result))
    .catch(err => res.status(500).send(err));
});

router.get('/:id', function(req, res) {
  userService.getById(req.params.id)
    .then(result => res.status(200).send(result))
    .catch(err => res.status(500).send(err));
});

module.exports = router;

له مساران: / للحصول على جميع المستخدمين و /:id للحصول على مستخدم واحد بواسطة ID. كما أنها تستخدم services/userService.js/ والتي لها طرق قائمة على الـpromise للحصول على هذه البيانات:

const users = [
  {id: '1', fullName: 'User The First'},
  {id: '2', fullName: 'User The Second'}
];

const getAll = () => Promise.resolve(users);
const getById = (id) => Promise.resolve(users.find(u => u.id == id));

module.exports = {
  getById,
  getAll
};

هنا تجنبنا استخدام موصل قاعدة بيانات فعلي أو ORM (على سبيل المثال Mongoose أو Sequelize) ببساطة محاكاة جلب البيانات باستخدام (...)Promise.resolve.

مشاكل توجيه (routing) Express.js

نظراً إلى معالجات التوجيه الخاصة بنا، نرى أن كل مكالمة خدمة تستخدم عمليات استدعاء مكررة. .(...)then و (...)catch. لإرسال البيانات أو الأخطاء مرة أخرى إلى العميل.

للوهلة الأولى قد لا يبدو هذا الأمر مهماً. دعنا نضيف بعض المتطلبات الأساسية في العالم الحقيقي: سنحتاج فقط إلى عرض أخطاء معينة وحذف الأخطاء على مستوى-500 العامة أيضًا، سواء طبقنا هذا المنطق أم لا، فيجب أن مبنياً على البيئة. مع ذلك كيف سيبدو عندما ينمو مشروع المثال أعلاه من طريقين إلى مشروع حقيقي مع 200 طريق؟

النهج 1: دوال المنفعة (utility functions)

ربما ننشئ دوال المنفعة المنفصلة للتعامل مع resolve و reject وتطبيقها في كل مكان في مسارات Express.js الخاصة بنا:

// some response handlers in /utils 
const handleResponse = (res, data) => res.status(200).send(data);
const handleError = (res, err) => res.status(500).send(err);


// routes/users.js
router.get('/', function(req, res) {
  userService.getAll()
    .then(data => handleResponse(res, data))
    .catch(err => handleError(res, err));
});

router.get('/:id', function(req, res) {
  userService.getById(req.params.id)
    .then(data => handleResponse(res, data))
    .catch(err => handleError(res, err));
});

يبدو أفضل: نحن لا نكرر تنفيذنا لإرسال البيانات والأخطاء. لكننا سنظل بحاجة إلى استيراد هذه المعالجات في كل مسار وإضافتها إلى كل promise تم تمريره إلى ()thenو ()catch.

النهج 2: البرنامج الوسيط (middleware)

يمكن أن يكون الحل الآخر هو استخدام أفضل ممارسات Express.js حول الـpromises: نقل منطق إرسال الأخطاء إلى برنامج الوسيط Express.js (تمت إضافتها في app.js) وتمرير أخطاء غير متزامنة إليه باستخدام next لرد الاتصال. سيستخدم إعداد البرنامج الوسيط الأساسي للخطأ وظيفة مجهولة بسيطة:

app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.send(err);
});

ييتفهم Express.js أن هذا يتعلق بالأخطاء لأن توقيع الدالة يحتوي على أربع وسائط إدخال. (إنها تستفيد من حقيقة أن كل كائن دالة له خاصية length. التي تصف عدد المعلمات التي تتوقعها الوظيفة.)

سيبدو تمرير الأخطاء عبر next كما يلي:

// some response handlers in /utils 
const handleResponse = (res, data) => res.status(200).send(data);

// routes/users.js
router.get('/', function(req, res, next) {
  userService.getAll()
    .then(data => handleResponse(res, data))
    .catch(next);
});

router.get('/:id', function(req, res, next) {
  userService.getById(req.params.id)
    .then(data => handleResponse(res, data))
    .catch(next);
});

حتى باستخدام دليل الرسمي لأفضل الممارسات، ما زلنا بحاجة إلى JS promises في كل معالج مسار لحل استخدام وظيفة ()handleResponse ورفضها بتمرير الدالة next.

دعونا نحاول تبسيط ذلك من خلال نهج أفضل.

النهج 3: البرنامج الوسيط لـ promis-based

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

دالة ()promiseMiddleware الخاصة بنا

دعونا ننشئ برمجية وسيطة وعدنا ، والتي ستمنحنا المرونة في هيكلة مسارات Express.js الخاصة بنا بشكل أكثر أناقة. سنحتاج إلى ملف جديد، /middleware/promise.js جديد.

const handleResponse = (res, data) => res.status(200).send(data);
const handleError = (res, err = {}) => res.status(err.status || 500).send({error: err.message});


module.exports = function promiseMiddleware() {
  return (req,res,next) => {
    res.promise = (p) => {
      let promiseToResolve;
      if (p.then && p.catch) {
        promiseToResolve = p;
      } else if (typeof p === 'function') {
        promiseToResolve = Promise.resolve().then(() => p());
      } else {
        promiseToResolve = Promise.resolve(p);
      }

      return promiseToResolve
        .then((data) => handleResponse(res, data))
        .catch((e) => handleError(res, e));  
    };

    return next();
  };
}

في app.js دعنا نطبق البرنامج الوسيط الخاص بنا على كائن app لـ Express.js العام ونحدِّث سلوك الخطأ الافتراضي:

const promiseMiddleware = require('./middlewares/promise');
//...
app.use(promiseMiddleware());
//...
app.use(function(req, res, next) {
  res.promise(Promise.reject(createError(404)));
});
app.use(function(err, req, res, next) {
  res.promise(Promise.reject(err));
});

لاحظ أننا لا نحذف البرنامج الوسيط للأخطاء. لا يزال معالج الأخطاء مهمًا لجميع الأخطاء المتزامنة التي قد تكون موجودة الكود الخاص بنا. ولكن بدلاً من تكرار منطق إرسال الخطأ، تمرر البرنامج الوسيط للأخطء الآن، أي أخطاء متزامنة إلى نفس دالة ()handleError  المركزية عبر استدعاء ()Promise.reject  يُرسل إلى ()res.promise .

يساعدنا هذا في التعامل مع الأخطاء المتزامنة مثل هذا الخطأ:

router.get('/someRoute', function(req, res){
  throw new Error('This is synchronous error!');
});

أخيرًا دعنا نستخدم ()res.promise الجديد في /routes/users.js:0

const express = require('express');
const router = express.Router();

const userService = require('../services/userService');

router.get('/', function(req, res) {
  res.promise(userService.getAll());
});

router.get('/:id', function(req, res) {
  res.promise(() => userService.getById(req.params.id));
});

module.exports = router;

لاحظ الاستخدامات المختلفة لـ .promise() : يمكننا منحها function أو promise. يمكن أن تساعدك دوال التمرير في الأساليب التي لا تحتوي على promises. يرى .promise() أنها دالة ويغلفها بـ promise.

أين من الأفضل بالفعل إرسال الأخطاء إلى العميل؟ إنه سؤال جيد حول تنظيم الكود. يمكننا فعل ذلك في البرنامج الوسيط للأخطاء (لأنه من المفترض أن تعمل مع الأخطاء) أو في البرنامج الوسيط التي نعد بها (لأنها تحتوي بالفعل على تفاعلات مع كائن ردنا). قررت الاحتفاظ بجميع عمليات الاستجابة في مكان واحد في البرامج الوسيطة التي وعدنا بها ، لكن الأمر متروك لكل مطور لتنظيم الكود الخاص به.

من الناحية الفنية فإن () res.promise اختياري

لقد أضفنا res.promise ()لكننا لسنا مقيدين باستخدامه: نحن أحرار في التعامل مع كائن الاستجابة مباشرةً عندما نحتاج إلى ذلك. دعونا نلقي نظرة على حالتين، إذ سيكون ذلك مفيدًا: إعادة التوجيه وتدفق الأنابيب.

الحالة الخاصة 1: إعادة التوجيه (ridirecting)

لنفترض أننا نريد إعادة توجيه المستخدمين إلى عنوان URL آخر. دعونا نضيف دالة ()getUserProfilePicUrl  في userService.js:

const getUserProfilePicUrl = (id) => Promise.resolve(`/img/${id}`);

والآن دعونا نستخدمه في جهاز توجيه مستخدمينا بأسلوبasync / await مع معالجة استجابة مباشرة:

router.get('/:id/profilePic', async function (req, res) {
  try {
    const url = await userService.getUserProfilePicUrl(req.params.id);
    res.redirect(url);
  } catch (e) {
    res.promise(Promise.reject(e));
  }
});

لاحظ كيف نستخدم حالة async / await ونقوم بإعادة التوجيه و (الأهم) لا يزال لدينا مكان مركزي واحد لتمرير أي خطأ لأننا استخدمنا ()res.promise لمعالجة الأخطاء.

الحالة الخاصة 2: تدفق الأنابيب (stream piping)

مثل مسار صورة ملفنا الشخصي، يعد تدفق الأنانيب حالة أخرى نحتاج فيها إلى معالجة كائن الاستجابة مباشرة

للتعامل مع الطلبات إلى عنوان URL الذي نعيد التوجيه إليه الآن دعنا نضيف مسارًا يعرض بالصور العامة.

أولاً يجب أن نضيف profilePic.jpg في مجلد فرعي جديد /assets/img (في مشروع حقيقي سنستخدم التخزين السحابي مثل AWS S3 لكن آلية الأنابيب ستكون هي نفسها.)

فلننقل هذه الصورة استجابةً لطلبات /img/profilePic/:id. نحن بحاجة إلى إنشاء جهاز توجيه جديد لذلك في /routes/img.js :

const express = require('express');
const router = express.Router();

const fs = require('fs');
const path = require('path');

router.get('/:id', function(req, res) {
  /* Note that we create a path to the file based on the current working
   * directory, not the router file location.
   */

  const fileStream = fs.createReadStream(
    path.join(process.cwd(), './assets/img/profilePic.png')
  );
  fileStream.pipe(res);
});

module.exports = router;

ثم نضيف جهاز التوجيه /img الجديد في app.js :

app.use('/users', require('./routes/users'));
app.use('/img', require('./routes/img'));

من المحتمل أن يبرز اختلاف واحد مقارنة بحالة إعادة التوجيه: لم نستخدم ()res.promise  في جهاز التوجيه img/ ويرجع ذلك إلى أن سلوك كائن الاستجابة الموجه بالفعل والذي يتم تمريره لخطأ سيكون مختلفًا عما إذا حدث الخطأ في منتصف التدفق.

يحتاج مطورو Express.js إلى الانتباه عند العمل مع التدفقات في تطبيقات Express.js، ومعالجة الأخطاء بشكل مختلف اعتمادًا على وقت حدوثها. نحتاج إلى معالجة الأخطاء قبل أن تساعدنا الأنابيب (()res.promise في ذلك) وكذلك في منتصف الطريق (استنادًا إلى معالج .on('error')، ولكن مزيدًا من التفاصيل خارج نطاق هذه المقالة.

تحسين ()res.promise

كما هو الحال مع استدعاء ()res.promise  لسنا مقيدًا بتطبيقه بالطريقة التي نطبقها أيضًا. يمكن زيادة الوعد Middleware.js لقبول بعض الخيارات في ()res.promise  للسماح للمتصلين بتحديد أكواد حالة الاستجابة أو نوع المحتوى أو أي شيء آخر قد يتطلبه المشروع. يعود الأمر للمطورين لتشكيل أدواتهم وتنظيم الأكواد الخاصة بهم بحيث تناسب احتياجاتهم على أفضل وجه.

معالجة أخطاء Express.js تلبي الترميز الحديث لـ promise-based

يتيح النهج المقدم هنا معالجات مسار أكثر أناقة مما بدأنا به ونقطة واحدة من نتائج المعالجة والأخطاء – حتى تلك التي تم إطلاقها خارج نطاق res.promise (…) – بفضل معالجة الأخطاء في app.js. ومع ذلك ، فنحن لسنا مجبرين على استخدامه ويمكننا معالجة الحالات المتطورة كما نريد.

الكود الكامل من هذه الأمثلة متاح على GitHub. من هناك يمكن للمطورين إضافة منطق مخصص حسب الحاجة إلى دالة ()handleResponse  مثل تغيير حالة الاستجابة إلى 204 بدلاً من 200 في حالة عدم توفر بيانات.

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

  • تنسيق جميع الأخطاء بشكل متسق كـ {error: {message}}  
  • أرسل رسالة عامة إذا لم يتم تقديم أي حالة أو تمرير رسالة معينة بخلاف ذلك.
  • إذا كانت البيئةdev (أو test وما إلى ذلك) فقم بتعبئة حقل error.stack .
  • معالجة أخطاء فهرس قاعدة البيانات (على سبيل المثال ، يوجد بالفعل كيان يحتوي على حقل مفهرس فريد) والاستجابة بأمان مع أخطاء المستخدم ذات المعنى.

كان منطق مسار Express.js هذا في مكان واحد ، دون لمس أي خدمة – وهو الفصل الذي ترك الرمز أسهل كثيرًا في الحفاظ عليه وتوسيعه. هذه هي الطريقة التي يمكن للحلول البسيطة – ولكن الأنيقة – أن تحسن هيكل المشروع بشكل كبير.

المصدر

منشور ذات صلة

اترك تعليقاً

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

السلة