ایجاد ExpressJS خود از ابتدا (قسمت 1) – مبانی، روش ها و مسیریابی

سلام، آیا تا به حال به ایجاد چارچوب وب خود با استفاده از NodeJS فکر کرده اید تا بفهمید که در زیر هود چه اتفاقی می افتد، روش ها، میان افزارها، کنترلرها و غیره…
من می دانم که ما فریمورک های بسیار خوبی برای ایجاد برنامه های کاربردی عالی داریم، مانند ExpressJS، NestJS، Fastify، و غیره. اما می دانید که آنها چه کار می کنند، فکر می کنم اکثر توسعه دهندگان حتی به آن اهمیتی نمی دهند، فقط می خواهند ایجاد کنند. برنامه های آنها، و این مشکلی ندارد، اما ممکن است آنها تجربه بهتری در ایجاد برنامه ها و درک آنچه که فریم ورک انجام می دهد داشته باشند، اجازه می دهد قابلیت های فریم ورک را گسترش دهد، برخی از خطاهای غیرمنتظره را برطرف کند، مشکلات عملکرد را بهبود بخشد، به طور کلی زندگی آنها را آسان تر کند.
در طول مسیر ایجاد چارچوب وب، ما با چندین مفهوم مرتبط با جاوا اسکریپت، NodeJS و به طور کلی برنامه نویسی مانند الگوهای طراحی و عبارات منظم کار می کنیم.
کاربرد: امروز می خواهیم اولین قسمت از Web Framework خود را ایجاد کنیم تا بتوانیم مسیرها را ایجاد کنیم و این مسیرها را مدیریت کنیم.
الزامات:
برپایی
ما در اینجا می خواهیم یک راه اندازی اولیه ایجاد کنیم تا بتوانیم به کار ادامه دهیم.
mkdir web-framework
cd web-framework
npm init -y
- ترمینال مورد نظر خود را باز کنید
- یک پوشه با نام مورد نظر خود ایجاد کنید.
- در پوشه وارد کنید
- یک پروژه جدید npm را شروع کنید
واردات
src/app.js
const { createServer } = require('http')
const { match } = require('path-to-regexp')
خط اول واردات createServer
تابعی از Node.js داخلی http
ماژول، که به ما اجازه می دهد یک سرور HTTP ایجاد کنیم. خط دوم تابع مطابقت را از path-to-regexp
بسته، که به ما امکان می دهد URL ها را با مسیرهای خاص مطابقت دهیم.
برای نصب path-to-regex
بسته:
npm install path-to-regex
کاربرد
src/app.js
const App = () => {
const routes = new Map()
const createMyServer = () => createServer(serverHandler.bind(this))
را App
تابع نقطه ورود اصلی برای چارچوب برنامه وب ما است. جدید ایجاد می کند Map
شی به نام routes برای ذخیره تمام مسیرهای تعریف شده ما. همچنین یک تابع کمکی به نام تعریف می کند createMyServer
که یک سرور HTTP با استفاده از serverHandler
تابع، تابعی که به زودی ایجاد خواهیم کرد، کنترل کننده اصلی خواهد بود.
تعریف مسیر
قصد من این نیست که همه روش های HTTP را پوشش دهم، بلکه فقط برخی از آنها را به عنوان مثال نشان می دهم.
src/app.js
const get = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/GET`) || []
routes.set(`${path}/GET`, [...currentHandlers, ...handlers])
}
const post = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/POST`) || []
routes.set(`${path}/POST`, [...currentHandlers, ...handlers])
}
const put = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/PUT`) || []
routes.set(`${path}/PUT`, [...currentHandlers, ...handlers])
}
const patch = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/PATCH`) || []
routes.set(`${path}/PATCH`, [...currentHandlers, ...handlers])
}
const del = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/DELETE`) || []
routes.set(`${path}/DELETE`, [...currentHandlers, ...handlers])
}
اینها توابع تعریف مسیر هستند که یک مسیر جدید را تعریف می کنند و کنترل کننده ها را برای هر روش HTTP (GET، POST، PUT، PATCH، DELETE) مشخص می کنند. الف می گیرند path
آرگومان و هر تعداد از handlers
. هر تابع کنترل کننده های فعلی را برای متد و مسیر مشخص شده از تابع بازیابی می کند routes
شیء، هر کنترل کننده جدیدی را اضافه می کند و کنترل کننده های به روز شده را مجدداً در قسمت تنظیم می کند routes
هدف – شی.
الگوی مطابق با مسیر و یافتن کنترل کننده مناسب باید شامل مسیر و در پایان نام متد باشد. مانند نمونه های زیر:
برای /test/1/GET
مسیر است /test/1
و روش این است GET
برای /test1/2/test2/POST
مسیر است /test1/2/test2
و روش این است POST
را Map
راه خوبی برای ذخیره سازی هندلرها است زیرا در اینجا می توانیم از برخی توابع مفید مانند استفاده کنیم get
و keys
.
درمان URL ها
برای اینکه سیستم مسیریابی ما به خوبی کار می کند، باید با URL ها رفتار کنیم، زیرا آنها آنطور که ما انتظار داریم نمی آیند.
src/app.js
const sanitizeUrl = (url, method) => {
// Get the last part of the URL, removing the domain
const urlParams = url.split('/').slice(1)
// Remove querystrings from the last parameter
const [lastParam] = urlParams[urlParams.length - 1].split('?')
urlParams.splice(urlParams.length - 1, 1)
// Create the URL with our pattern
const allParams = [...urlParams, lastParam].join('/')
const sanitizedUrl = `/${allParams}/${method.toUpperCase()}`
return sanitizedUrl
}
const matchUrl = (sanitizedUrl) => {
for (const path of routes.keys()) {
const urlMatch = match(path, {
decode: decodeURIComponent,
})
const found = urlMatch(sanitizedUrl)
if (found) {
return path
}
}
return false
}
اینها توابع کمکی هستند که برای پاکسازی و تطبیق URLها با مسیرهای مربوطه استفاده می شوند. sanitizeUrl
یک URL و یک روش HTTP را می گیرد و هر رشته پرس و جو را حذف می کند، سپس پارامترها را به یک الگوی مطابق با ساختار مسیر متصل می کند. matchUrl
از طریق مسیرها بررسی می کند که آیا URL با یکی از مسیرهای فعلی ما مطابقت دارد یا خیر.
مدیریت سرور
src/app.js
const serverHandler = async (request, response) => {
const sanitizedUrl = sanitizeUrl(request.url, request.method)
const match = matchUrl(sanitizedUrl)
if (match) {
const middlewaresAndControllers = routes.get(match)
console.log(middlewaresAndControllers)
response.statusCode = 200
response.end('Found')
} else {
response.statusCode = 404
response.end('Not found')
}
}
ابتدا تابع تماس می گیرد sanitizeUrl()
با request.url
و request.method
به عنوان آرگومان هایی برای ایجاد یک URL سالم، که برای بررسی مسیرهای تنظیم شده در چارچوب استفاده می شود.
سپس تابع فراخوانی می کند matchUrl()
با URL پاکسازیشده برای یافتن مسیری منطبق که در چارچوب تنظیم شده است. اگر مطابقت پیدا شود، لیست میان افزارها و کنترلرهای مرتبط با آن مسیر را از نقشه مسیرها بازیابی می کند.
اگر میانافزارها و/یا کنترلکنندههای مرتبط با مسیر وجود داشته باشد، تابع آنها را در کنسول ثبت میکند و کد وضعیت پاسخ را روی 200 تنظیم میکند که نشان میدهد درخواست موفقیتآمیز بوده است. در نهایت، پاسخ را با پیام به پایان می رساند 'Found'
.
اگر مطابقت پیدا نشد، تابع کد وضعیت پاسخ را روی 404 تنظیم می کند که نشان می دهد منبع درخواستی پیدا نشد. در نهایت، پاسخ را با پیام به پایان می رساند 'Not found'
.
تابع Run and Exports
src/app.js
const run = (port) => {
const server = createMyServer()
server.listen(port)
}
return {
run,
get,
post,
patch,
put,
del
}
را run
تابع مسئول راه اندازی سرور با فراخوانی روش گوش دادن نمونه سرور است. را listen
متد یک شماره پورت را به عنوان آرگومان می پذیرد، که درگاهی است که سرور به درخواست های دریافتی گوش می دهد.
در این تابع، یک نمونه جدید از سرور با فراخوانی ایجاد می شود createMyServer
تابعی که قبلا در کد تعریف شده است. سپس listen
متد در نمونه سرور فراخوانی می شود و در آرگومان پورت ارائه شده به سرور ارسال می شود run
تابع.
در نهایت، ما می توانیم تمام توابع عمومی را به عنوان یک صادر کنیم closure
بازگرداندن آنها
کد کامل
src/app.js
const { createServer } = require('http')
const { match } = require('path-to-regexp')
const App = () => {
const routes = new Map()
const createMyServer = () => createServer(serverHandler.bind(this))
const get = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/GET`) || []
routes.set(`${path}/GET`, [...currentHandlers, ...handlers])
}
const post = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/POST`) || []
routes.set(`${path}/POST`, [...currentHandlers, ...handlers])
}
const put = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/PUT`) || []
routes.set(`${path}/PUT`, [...currentHandlers, ...handlers])
}
const patch = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/PATCH`) || []
routes.set(`${path}/PATCH`, [...currentHandlers, ...handlers])
}
const del = (path, ...handlers) => {
const currentHandlers = routes.get(`${path}/DELETE`) || []
routes.set(`${path}/DELETE`, [...currentHandlers, ...handlers])
}
const sanitizeUrl = (url, method) => {
const urlParams = url.split('/').slice(1)
// remove querystrings from the last parameter
const [lastParam] = urlParams[urlParams.length - 1].split('?')
urlParams.splice(urlParams.length - 1, 1)
// create the URL with our pattern
const allParams = [...urlParams, lastParam].join('/')
const sanitizedUrl = `/${allParams}/${method.toUpperCase()}`
return sanitizedUrl
}
const matchUrl = (sanitizedUrl) => {
for (const path of routes.keys()) {
const urlMatch = match(path, {
decode: decodeURIComponent,
})
const found = urlMatch(sanitizedUrl)
if (found) {
return path
}
}
return false
}
const serverHandler = async (request, response) => {
const sanitizedUrl = sanitizeUrl(request.url, request.method)
const match = matchUrl(sanitizedUrl)
if (match) {
const middlewaresAndControllers = routes.get(match)
console.log(middlewaresAndControllers)
response.statusCode = 200
response.end('Found')
} else {
response.statusCode = 404
response.end('Not found')
}
}
const run = (port) => {
const server = createMyServer()
server.listen(port)
}
return {
run,
get,
post,
patch,
put,
del
}
}
module.exports = App
استفاده و تست
من یک فایل جداگانه ایجاد کردم به نام index.js
بیرون از src
پوشه برای تست چارچوب ما.
index.js
const App = require('./src/app')
const app = App()
app.get('/test/test2', function test() { }, function test2() { })
app.post('/test', (req, res) => console.log('test'))
app.patch('/test', (req, res) => console.log('test'))
app.put('/test', (req, res) => console.log('test'))
app.del('/test', (req, res) => console.log('test'))
const start = async () => {
app.run(3000)
}
start()
می توانید این فایل را اجرا کرده و با استفاده از سرویس گیرنده HTTP مورد علاقه خود برنامه را آزمایش کنید.
node index.js
مراحل بعدی
در آموزش بعدی در حال کار با middlewares
و controllers
. اگر از این آموزش خوشتان آمد، در واکنش خود دریغ نکنید و من را دنبال کنید تا از ابتدا آموزش های بعدی من را ببینید. من سعی می کنم حداقل یک آموزش در هفته بنویسم.
تشکر به زودی میبینمت!