مقدمه

برای کار با Express، درک مکانیزم مسیریابی (Routing) در این فریمورک، بسیار مهم و کلیدی است. یادآوری می‌کنم که مسیریابی در یک فریمورک وب مانند Express به ساز و کاری گفته می‌شود که درخواست‌ها را به کدهای پردازش‌کننده‌ی آنها هدایت می‌کند. در این درس، در مورد مفهوم کلیدی مسیریابی در Express صحبت می‌کنیم و جزئیات مربوط به بخش‌های مختلف یک route را بیان خواهیم کرد.

آناتومی یک Route در Express

همانطور که در درس قبل دیدیم، یک route در Express دارای فرم کلی زیر است.

app.method(path, handler) {
  //handler code
}

که در آن:

  • method نام متدی است که نوع درخواست را مشخص می‌کند و متناظر با یک متد HTTP است. این متد برای درخواست‌های GET برابر با get و برای درخواست‌های POST برابر با post است.
  • path بخشی از URL درخواست است که مشخص می‌کند چه چیزی درخواست شده است.
  • handler یک تابع callback است که شامل کد پردازش‌کننده‌ی درخواست است.

بنابراین، یک route به فرمی که در بالا می‌بینید، به این معناست که: اگر یک درخواست از نوع method و با مسیر path به دست وب‌سرور برسد، کدی که درون بدنه‌ی handler قرار دارد، به عنوان پاسخ، اجرا شود.

متد مورد استفاده در یک route می‌تواند یکی از موارد زیر باشد:

  • متد get: برای هندل درخواست‌هایی که متد HTTP آنها GET است، کاربرد دارد. وقتی کاربری آدرس URL یک صفحه‌ی وب را در نوار آدرس مرورگر وارد می‌کند یا روی یک لینک کلیک می‌کند، یک درخواست از نوع GET برای مشاهده‌ی آن صفحه ارسال می‌شود. پس، چنین درخواست‌هایی را باید با استفاده از یک متد get هندل کرد.
  • متد post: برای هندل درخواست‌هایی که متد HTTP آنها POST است، کاربرد دارد. معمولاً درخواست‌های POST با ارسال داده برای سرور همراه هستند و به پردازش‌های متفاوتی نسبت به درخواست‌های GET نیاز دارند. برای مثال، وقتی کاربری یک فرم را پر می‌کند و آن را برای سرور ارسال می‌کند، یک درخواست از نوع POST ارسال می‌شود و بنابراین، برای هندل آن درخواست باید از متد post استفاده کرد.
  • متد put: برای هندل درخواست‌هایی که متد HTTP آنها PUT است، کاربرد دارد. متد PUT برای آپدیت یا جایگزینی کامل داده‌های یک منبع به کار می‌رود.
  • متد delete: برای هندل درخواست‌هایی که متد HTTP آنها DELETE است، کاربرد دارد. از متد DELETE برای حذف یک منبع استفاده می‌شود.
  • سایر متدها: متدهای دیگری مانند trace و head و options هم وجود دارند که هر کدام متناظر با یک درخواست HTTP همنام با خود هستند. این متدها با اهداف آزمایشی و تشخیصی یا برای دیباگ به کار می‌روند و کمتر مورد استفاده قرار می‌گیرند.

مؤلفه‌ی path در یک route یک رشته است که برای ایجاد تناظر بین درخواست و پاسخ استفاده می‌شود. توجه داشته باشید که بخش path در یک route یک ماهیت منطقی دارد نه فیزیکی. به عبارت دیگر، path به یک ساختار دایرکتوری فیزیکی اشاره نمی‌کند و Express آن را صرفاً به صورت یک رشته (string) می‌بیند و آن رشته را به یک تابع handler متناظر می‌کند.

سومین و آخرین مؤلفه‌ی یک route بخش handler است. مؤلفه‌ی handler یک تابع callback است که در پاسخ به یک درخواست اجرا می‌شود. یک تابع handler به دو شیء req و res دسترسی دارد و بنابراین، به فرم زیر پیاده‌سازی می‌شود.

app.get('/hello', (req, res) => {
  res.send("Hello, world!");
});

در اینجا از سینتکس arrow functions در جاوااسکریپت استفاده شده تا یک تابع callback برای متد get تعریف شود. دو آرگومان req و res هم به این تابع پاس شده که اولی یعنی req شیء درخواست است و به پراپرتی‌های مربوط به درخواست (request) دسترسی دارد و دومی یعنی res شیء پاسخ نامیده می‌شود و به پراپرتی‌های مربوط به پاسخ (response) دسترسی دارد.

یکی از پراپرتی‌های شیء پاسخ که در بالا از آن استفاده شده، متد send() است که یک رشته را به عنوان پاسخ ارسال می‌کند. البته درون رشته می‌توان مانند زیر از عناصر HTML هم استفاده کرد.

app.get('/', (req, res) => {
  res.send('<h1>Welcome</h1>');
});

در درس بعد خواهیم دید که چطور می‌توانیم یک سند HTML را به عنوان پاسخ یک درخواست برگردانیم و چطور می‌توانیم محتوای سند برگشتی را به صورت دینامیک تولید کنیم.

بسیار خوب، حالا که با نحوه‌ی ایجاد یک route و ساختار آن آشنا شدیم، در ادامه به این موضوع می‌پردازیم که داده‌هایی که به همراه درخواست‌ها برای سرور ارسال می‌شوند، در سمت سرور چطور در دسترس تابع handler قرار می‌گیرند. این موضوع را برای درخواست‌های GET و POST به صورت مجزا بررسی می‌کنیم.

مذیریت درخواست‌های GET

اگرچه درخواست‌های GET معمولاً برای دریافت اطلاعات از سرور به کار می‌روند، اما امکان ارسال داده برای سرور را هم دارند. داده‌های ارسالی که کوئری نامیده می‌شوند، در قالب جفت‌های key و value به انتهای URL درخواست (بعد از یک کاراکتر ?) اضافه می‌شوند. اگر بیش از یک کوئری وجود داشته باشد، از کاراکتر & برای جدا کردن آنها از هم استفاده می‌شود.

با این حساب، ساختار کلی URL یک درخواست GET به صورت زیر است:

http://example.com:80/hello/world?key1=value1&key2=value2

این URL نمونه از بخش‌های زیر تشکیل شده است:

  • پروتکل: پروتکل وب یعنی HTTP یک نسخه‌ی امن به نام HTTPS هم دارد. به علاوه، مرورگرها از سرویس‌های دیگری به‌جز وب هم پشتیبانی می‌کنند و بنایراین، پروتکل‌های دیگری را هم شامل هستند. پس، قبل از هر چیز باید پروتکل مورد نظر را مشخص کرد. در نمونه‌ی بالا عبارت http نشان می‌دهد که درخواست باید با استفاده از پروتکل HTTP ارسال شود.
  • نام دامنه: این بخش مشخص می‌کند که درخواست باید به چه مقصدی ارسال شود. همانطور که می‌دانید، هر نام دامنه یا domain name متناظر با یک آدرس ip است که سرور مقصد را مشخص می‌کند.
  • پورت: وارد کردن پورت در یک URL اختیاری است؛ مگر اینکه پورت‌های پیش‌فرض یعنی 80 برای HTTP و 443 برای HTTPS را تغییر داده باشیم.
  • مسیر: بخش مسیر یا path در یک URL همان رشته‌ای است که در یک route از آن برای متناظر کردن درخواست به یک تابع handler استفاده می‌شود.
  • کوئری‌ها: جفت‌های key=value که بعد از کاراکتر ? در انتهای یک URL قرار می‌گیرند، داده‌هایی هستند که همراه درخواست برای سرور ارسال می‌شوند.

URL نمونه‌ی بالا یک URL مطلق است که با پروتکل و نام دامنه شروع می‌شود اما یک URL نسبی نیازی به این بخش‌ها ندارد و با path شروع می‌شود.

حالا اجازه دهید ببینیم در سمت سرور چطور به بخش‌های مختلف URL یک درخواست دسترسی داریم. شیء درخواست یعنی req این امکان را برای متد handler فراهم می‌کند که به بخش‌های مختلف URL درخواست دسترسی داشته باشد. این شیء دارای چند پراپرتی است که به بخش‌های مختلف URL درخواست اشاره می‌کنند. جدول زیر، این پراپرتی‌ها را لیست کرده و مقدار هر پراپرتی را برای URL نمونه‌ی بالا در ستون مثال آورده است.

پراپرتی توضیح مثال
req.url این پراپرتی که قبلاً هم آن را دیده‌ایم، URL مربوط به درخواست را (بدون پروتکل و دامنه) برمی‌گرداند. /hello/world?key1=value1&key2=value2
req.protocol پروتکل درخواست (http یا https) را در قالب یک رشته برمی‌گرداند. 'http'
req.hostname نام دامنه‌ی موجود در URL درخواست را به صورت رشته برمی‌گرداند. 'example.com'
req.path فقط قسمت path موجود در URL را برمی‌گرداند. '/path/to/myfile'
req.params پارامترهای موجود در path را بهصورت یک شیء برمی‌گرداند. {}
req.query یک شیء شامل پارامترهای کوئری را برمی‌گرداند. {key1:'value1', key2:'value2'}

اجازه دهید پراپرتی‌های جدول بالا و نحوه‌ی استفاده از آنها برای دسترسی به بخش‌های مختلف URL یک درخواست را در یک مثال ببینیم.

Copy Icon app.js
const express = require('express');
const morgan = require('morgan');
            
const app = express();
            
app.use(morgan('dev'));
            
app.get('/hello/world', (req, res) => {
  res.send(`
    <p>req.url = ${req.url}</p>
    <p>req.protocol = ${req.protocol}</p>
    <p>req.hostname = ${req.hostname}</p>
    <p>req.path = ${req.path}</p>
    <p>req.query.name = ${req.query.name}</p>
    <p>req.query.age = ${req.query.age}</p>
    `);
});
            
const port = 3000;

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

حالا در حالی که سرور در حال اجراست، از طریق یک مرورگر، URL زیر را درخواست کنید و نتیجه را ببینید.

http://localhost:3000/hello/world?name=john&age=25

برای شیء req علاوه بر پراپرتی‌های موجود در مثال بالا، یک پراپرتی مهم دیگر با نام params هم وجود دارد که پارامترهای path یک URL را در دسترس تابع handler قرار می‌دهد. مثال زیر را ببینید.

Copy Icon app.js
app.get('/user/:id', (req, res) => {
  const userId = req.params.id;
  res.send(`User ID is ${userId}`);
});

پارامترهای path برای ایجاد مسیرهای دینامیک کاربرد دارند. برای مثال، پارامتر id در کد بالا می‌تواند هر مقداری را دریافت کند. یک درخواست که بخش path در آن /user/123 باشد، باعث می‌شود که مقدار 123 به پارامتر id اختصاص داده شود و یک درخواست به فرم /user/abc باعث تخصیص مقدار abc به پارامتر id می‌شود.

مدیریت درخواست‌های POST

دیدیم که امکان ارسال داده برای سرور با استفاده از متدهای GET هم وجود دارد اما این کار با محدودیت‌هایی همراه است که کاربرد آن را به موارد خاص محدود می‌کند. مهمترین محدودیتی که ارسال داده‌ها با GET دارد این است که (همانطور که دیدیم) این داده‌ها در URL نمایش داده می‌شوند و بنابراین، برای ارسال داده‌های محرمانه و حساس نمی‌توان از این روش استفاده کرد و در عوض، باید از متد POST استفاده کرد.

درخواست‌های POST داده‌ها را در بدنه‌ی درخواست قرار داده و ارسال می‌کنند و بنابراین، URL یک درخواست POST فاقد بخش کوئری است. در سمت سرور، با استفاده از یک پراپرتی دیگر از شیء req با نام body به داده‌های ارسالی توسط یک درخواست POST دسترسی داریم.

اما برای اینکه داده‌ها از بدنه‌ی درخواست استخراج شده و در پراپرتی req.body قرار گیرند، باید از یک middleware مناسب استفاده کنیم. این middleware از روی فرمت داده‌های ارسالی تعیین می‌شود. داده‌ها معمولاً یا به فرمت JSON ارسال می‌شوند و یا در قالب فرم‌های وب ارسال می‌شوند و نتیجتاً دارای فرمت application/x-www-form-urlencoded هستند. در ادامه، خواهیم دید که در مورد هر یک از این فرمت‌ها، چطور می‌توانیم داده‌ها را استخراج کنیم تا از طریق پراپرتی req.body در دسترس قرار گیرند.

دسترسی به داده‌های فرم

داده‌های یک فرم وب در قالب یک درخواست POST برای سرور ارسال می‌شوند. هر فیلد از فرم باید به روشی، مقداری را برای صفت‌های name و value فراهم کند و با ارسال فرم، این داده‌ها در بدنه‌ی درخواست POST جاسازی می‌شوند و در سمت سرور، با استفاده از یک middleware این داده‌ها از درخواست استخراج شده و در پراپرتی req.body قرار می‌گیرند.

داده‌های ارسالی توسط فرم‌های وب دارای فرمت application/x-www-form-urlencoded هستند و برای استخراج آنها می‌توانیم از یک پکیج به نام body-parser استفاده کنیم. قبلاً باید از یک کامند npm install body-parser برای نصب این پکیج استفاده می‌کردیم اما در نسخه‌های اخیر، این پکیج بخشی از Express است و به نصب جداگانه نیاز ندارد. تنها کاری که باید انجام دهیم، این است که با استفاده از یک تابع use() تابعی به نام urlencoded() را به صورت زیر به عنوان یک middleware سِت کنیم.

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

اجازه دهید با بررسی یک مثال، این مورد را در عمل ببینیم. یک صفحه‌ی وب با نام index.html درون دایرکتوری پروژه ایجاد کنید که شامل کد زیر باشد.

Copy Icon index.html
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>doctitle</title>
</head>
<body>
  <h1>Hi</h1>
  <form action="/hello" method="post">
    <div>
      <label for="user">Username: </label>
      <input type="text" id="user" name="username">
    </div>
    <div>
      <label for="pass">Password</label>
      <input type="password" id="pass" name="password">
    </div>
    <input type="submit" value="Submit">
  </form>
</body>
</html>

همانطور که می‌دانید، فیلدهای از نوع text و password باید یک مقدار را صراحتاً برای صفت name تعیین کنند و مقدار صفت value هم برای آنها مقداری است که در زمان ارسال، درون این فیلدها قرار دارد (و البته یک مقدار پیش‌فرض هم می‌توان برای صفت value تدارک دید).

حالا فایل app.js را باز کنید و کد زیر را درون آن قرار دهید.

Copy Icon app.js
const express = require('express');
const morgan = require('morgan');
            
const app = express();
            
app.use(morgan('dev'));
app.use(express.urlencoded({extended: false}));
            
app.post('/hello', (req, res) => {
  res.send(`Username: ${req.body.username}`);
});
            
const port = 3000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

فایل index.html را با یک مرورگر باز کنید، فیلدهای فرم را پر کنید و روی دکمه‌ی Submit کلیک کنید تا داده‌های فرم برای وب‌سرور ارسال شوند. در نتیجه، باید عبارت Username: x برای شما برگردانده شود که x مقداری است که در فیلد مربوط به username وارد کرده بودید.

دسترسی به داده‌های JSON

برای استخراج داده‌هایی که به فرمت JSON برای سرور ارسال شده‌اند، می‌توانیم از یک تابع دیگرِ Express با نام json() استفاده کنیم. مثل قبل، باید این تابع را با استفاده از یک تابع use() به عنوان یک middleware سِت کنیم.

Copy Icon app.js
app.use(express.json());

روش دسترسی به داده‌های JSON کاملاً مشابه داده‌های فرم است. برای ارسال داده‌ها به فرمت JSON می‌توانید از جاوااسکریپت در سمت کاربر و تکنولوژی‌هایی مثل Fetch API یا AJAX استفاده کنید.