شعار 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 و تجزیه کوکی اضافه می کنیم. سپس a را اضافه می کنیم usersRouter
برای /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 express = require('express');
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
برای بدست آوردن یک کاربر واحد با شناسه همچنین استفاده می کند /services/userService.js
، که روش های مبتنی بر وعده برای به دست آوردن این داده ها دارد:
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
};
در اینجا ما از استفاده از یک رابط DB واقعی یا ORM (به عنوان مثال، Mongoose یا Sequelize)، به سادگی تقلید واکشی داده ها با Promise.resolve(...)
.
مشکلات مسیریابی Express.js
با نگاهی به گردانندگان مسیر خود، می بینیم که هر تماس سرویس از موارد تکراری استفاده می کند .then(...)
و .catch(...)
پاسخ تماس برای ارسال داده ها یا خطاها به مشتری.
در نگاه اول، این ممکن است جدی به نظر نرسد. بیایید برخی از الزامات اساسی دنیای واقعی را اضافه کنیم: ما باید فقط خطاهای خاصی را نمایش دهیم و خطاهای سطح 500 عمومی را حذف کنیم. همچنین، اینکه آیا این منطق را اعمال می کنیم یا نه، باید بر اساس محیط باشد. با آن، زمانی که پروژه نمونه ما از دو مسیر خود به یک پروژه واقعی با 200 مسیر تبدیل شود، چگونه به نظر می رسد؟
رویکرد 1: توابع سودمند
شاید بتوانیم توابع ابزار مجزایی برای مدیریت ایجاد کنیم 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));
});
بهتر به نظر می رسد: ما اجرای ارسال داده ها و خطاها را تکرار نمی کنیم. اما همچنان باید این کنترلکنندهها را در هر مسیری وارد کنیم و آنها را به هر قولی اضافه کنیم then()
و catch()
.
رویکرد 2: میان افزار
راه حل دیگر می تواند استفاده از Express.js باشد بهترین شیوه ها حول وعدهها: منطق ارسال خطا را به میانافزار خطای Express.js منتقل کنید (اضافه شده است app.js
) و خطاهای async را با استفاده از 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 خود در هر گرداننده مسیر برای حل با استفاده از a نیاز داریم handleResponse()
تابع و رد با عبور از امتداد next
عملکرد.
بیایید سعی کنیم آن را با یک رویکرد بهتر ساده کنیم.
رویکرد 3: میان افزار مبتنی بر وعده
یکی از بزرگترین ویژگی های جاوا اسکریپت ماهیت پویا بودن آن است. در زمان اجرا می توانیم هر فیلدی را به هر شیء اضافه کنیم. ما از آن برای گسترش اشیاء نتیجه 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
، بیایید میان افزار خود را روی Express.js کلی اعمال کنیم app
شیء و به روز رسانی رفتار خطای پیش فرض:
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()
عملکرد از طریق a Promise.reject()
تماس ارسال شد به res.promise()
.
این به ما کمک می کند تا خطاهای همزمان مانند این را مدیریت کنیم:
router.get('/someRoute', function(req, res){
throw new Error('This is synchronous error!');
});
در نهایت، اجازه دهید از جدید خود استفاده کنیم res.promise()
که در /routes/users.js
:
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()
: ما می توانیم آن را یک تابع یا یک وعده منتقل کنیم. پاس کردن توابع می تواند به شما در روش هایی که قولی ندارند کمک کند. .promise()
می بیند که یک تابع است و آن را در یک قول می پیچد.
کجا بهتر است خطاها را برای مشتری ارسال کنیم؟ این یک سوال سازماندهی کد خوب است. ما میتوانیم این کار را در میانافزار خطای خود (چون قرار است با خطاها کار کند) یا در میانافزار قولمان (زیرا قبلاً با شی پاسخ ما تعامل دارد) انجام دهیم. من تصمیم گرفتم تمام عملیات پاسخگویی را در یک مکان در میان افزار قولمان نگه دارم، اما این به هر توسعه دهنده بستگی دارد که کد خود را سازماندهی کند.
از نظر فنی، res.promise()
اختیاری است
ما اضافه کرده ایم res.promise()
، اما ما در استفاده از آن قفل نیستیم: ما آزادیم که در صورت نیاز مستقیماً با شی پاسخ کار کنیم. بیایید به دو مورد که در آن مفید است نگاه کنیم: تغییر مسیر و لولهکشی جریان.
مورد ویژه 1: تغییر مسیر
فرض کنید می خواهیم کاربران را به 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: لوله کشی جریان
مانند مسیر تصویر نمایه ما، لوله کشی یک جریان موقعیت دیگری است که در آن باید مستقیماً شی پاسخ را دستکاری کنیم.
برای رسیدگی به درخواستها به 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()
، ما در آن قفل نیستیم اجرا کردن این راهی است که ما داریم. promiseMiddleware.js
را می توان برای پذیرش برخی گزینه ها افزایش داد res.promise()
به تماس گیرندگان اجازه می دهد تا کدهای وضعیت پاسخ، نوع محتوا یا هر چیز دیگری که ممکن است یک پروژه به آن نیاز داشته باشد را مشخص کنند. این به توسعه دهندگان بستگی دارد که ابزارهای خود را شکل دهند و کد خود را به گونه ای سازماندهی کنند که به بهترین نحو با نیازهای آنها مطابقت داشته باشد.
مدیریت خطای Express.js با کدنویسی مبتنی بر وعده مدرن روبرو می شود
رویکرد ارائه شده در اینجا اجازه می دهد گردانندگان مسیر زیباتر از چیزی که با آن شروع کردیم و الف نقطه واحد پردازش نتایج و خطاها– حتی آنهایی که بیرون از آن اخراج شده اند res.promise(...)
– به لطف رسیدگی به خطاها app.js
. با این حال، ما هستیم مجبور نیست از آن استفاده کنیم و بتوانیم موارد لبه را همانطور که می خواهیم پردازش کنیم.
کد کامل از این نمونه ها است در GitHub موجود است. از آنجا، توسعه دهندگان می توانند منطق سفارشی را در صورت نیاز به آن اضافه کنند handleResponse()
عملکرد، مانند تغییر وضعیت پاسخ به 204 به جای 200 در صورت عدم دسترسی به داده.
با این حال، کنترل اضافی بر روی خطاها بسیار مفیدتر است. این رویکرد به من کمک کرد تا به طور خلاصه این ویژگی ها را در تولید پیاده کنم:
- همه خطاها را به صورت پیوسته قالب بندی کنید
{error: {message}}
- در صورت عدم ارائه وضعیت، یک پیام عمومی ارسال کنید یا در غیر این صورت پیامی را ارسال کنید
- اگر محیط زیست باشد
dev
(یاtest
و غیره)، پر کنیدerror.stack
رشته - خطاهای فهرست پایگاه داده را مدیریت کنید (به عنوان مثال، برخی از موجودیتها با یک فیلد فهرستبندیشده منحصربهفرد از قبل وجود دارد) و با مهربانی با خطاهای معنیدار کاربر پاسخ دهید.
این منطق مسیر Express.js همه در یک مکان بود، بدون دست زدن به هیچ سرویسی – جدایی که باعث میشود حفظ و گسترش کد بسیار آسانتر شود. به این ترتیب راه حل های ساده – اما ظریف – می توانند ساختار پروژه را به شدت بهبود بخشند.