چگونه می‌توانید فریمورک وب خود را با Node.js بسازید

ترجمه و تالیف : عرفان حشمتی
تاریخ انتشار : 06 شهریور 99
خواندن در 4 دقیقه
دسته بندی ها : نود جی اس

این مقاله به شما می‌آموزد که فریمورک اکسپرس را تا حدودی پیاده‌سازی کنید. برای یادگیری خودتان بسیار عالی است اما در تولید از آن استفاده نکنید. مگر اینکه در مورد فضا یا پهنای باند با نصب NPM مشکل دارید.

دلیل نوشتن این نوع مقاله‌ها این نیست که ما بخواهیم مردم دوباره چرخ را اختراع کنند، بلکه تجربه کسب کنند. اگر npmjs را جستجو کنید، می‌توانید 100 ها برنامه را پیدا کنید که کم و بیش شبیه یکی از فریمورک‌های بزرگ شناخته شده مانند Express ، Nest ، Koa یا Fastify هستند. بنابراین چه چیزی باعث ایجاد یک فریمورک دیگر می‌شود؟ آیا این اتلاف وقت نیست؟ من فکر نمی‌کنم و دلیلش این است که شما می‌توانید با تلاش چیزهای زیادی برای خودتان یاد بگیرید. همچنین می‌توانید مهارت‌هایی کسب کنید که به شما در زندگی روزمره وب کمک می‌کند. همینطور می‌توانید ماتریکس را ببینید که می‌تواند به شما برای انجام کارهای OSS انگیزه دهد.

پیاده‌سازی فریمورک Express

در این مقاله، سعی کردیم بخشی از فریمورک Express را پیاده‌سازی کنیم. دقیقاً شامل چه قسمت‌هایی است؟

  • مسیرها، اکسپرس راهی برای ارتباط مسیرهای خاص دارد و در صورت برخورد یک مسیر، کد خاصی را اجرا می‌کند. همچنین شما می‌توانید مسیرها را بر اساس HTTP Verb متمایز کنید. بنابراین یک GET به /products با POST به /products متفاوت است.
  • Middleware، بخشی از کد است که می‌تواند قبل یا بعد از درخواست شما اجرا شود و حتی آنچه را که باید برای درخواست رخ دهد، کنترل کند. Middleware به این صورت است که چگونه می‌توانید یک هدر برای یک نشان خودکار را بررسی کنید و در صورت اعتبار، منابع درخواست شده را برگردانید. اگر نشانه معتبر نباشد، درخواست در آنجا متوقف می‌شود و می‌توان پیام مناسبی را نمایش داد.
  • پارامترهای پرس و جو، این قسمت پایانی URL است و می‌تواند به فیلتر کردن آنچه شما می‌خواهید پاسخ دهید، کمک کند. با توجه به URLای که اینگونه به نظر می‌رسد products?page=1&pagesize=20/، پارامترهای پرس و جو هر چیزی است که بعد از آن اتفاق می افتد؟
  • ارسال داده‌ها با body، داده‌ها را می‌توان از کلاینت به برنامه سرور ارسال کرد. همچنین می‌توان آن را از طریق URL یا از طریق body ارسال کرد. body می‌تواند چیزهای مختلفی داشته باشد، از JSON گرفته تا زمینه‌هایی با فرم ساده و حتی پرونده‌ها.

مثالی از برنامه با Express

بیایید چند خط اجرای برنامه Express را مرور کنیم. اتفاقات زیادی حتی با چند خط رخ می‌دهد:

const express = require('express')
const app = express();
app.get('/products/:id', (req, res) => {
  res.send(`You sent id ${req.params.id}`)
})

app.listen(3000, () => {
  console.log('Server up and running on port 3000')
})

یک برنامه HTTP

چگونه می‌توانیم به اجرای آن بپردازیم؟ خوب، ما ماژول HTTP را در اختیار داریم. بنابراین بیایید با یک اجرای بسیار کوچک بفهمیم که چه چیزی اتفاق افتاده است:

const http = require('http');
const PORT = 3000;

const server = http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('hello world');
});

server.listen(PORT, () => {
  console.log(`listening on port ${PORT}`)
})

ماژول HTTP فقط یک حس مسیریابی اساسی دارد. اگر به دنبال چنین برنامه‌ای با URL http: http://localhost:3000/products باشید، req.url شامل /products است و req.method شامل رشته get خواهد بود. این همان چیزی است که شما دارید.

پیاده‌سازی مسیریابی و HTTP

ما قصد داریم موارد زیر را پیاده‌سازی کنیم.

  • متدهای HTTP Verb، به متدهایی مثل get() و post() نیاز داریم.
  • پارامترهای مسیر و مسیریابی، ما باید بتوانیم با محصولات مطابقت داشته باشیم و باید بتوانیم شناسه پارامتر مسیر را از عبارتی مانند products/: id جدا کنیم.
  • پارامترهای پرس و جو، ما باید بتوانیم URL ای مانند http://localhost:3000/products?page=1&pageSize=20 را بگیریم و پارامترها و اندازه صفحه را بگیریم تا کار با آنها آسان باشد.

متدهای HTTP Verb

بیایید یک server.js ایجاد کنیم و سرور خود را مانند زیر شروع کنیم:

// server.js
const http = require('http')

function myServer() {
  let routeTable = {};
  http.createServer((req, res) => {

  });   

  return {
    get(path, cb) {
      routeTable[path] = { 'get': cb }
    }
  }
}

بیایید کد بالا را رها کنیم و به اجرای مسیریابی ادامه دهیم.

پارامترهای تجزیه مسیر

اجرای /products آسان است، فقط مقایسه رشته‌ای با RegEx یا بدون آن است. حذف پارامتر id از  products/: id/ کمی سخت‌تر است. ما می‌توانیم این کار را با یک RegEx انجام دهیم که متوجه شویم product/: id/ می‌تواند به عنوان RegEx /products/:(?<id>\w+) بازنویسی شود. این یک گروه به اصطلاح نامگذاری شده است که وقتی ما match() را اجرا می‌کنیم، یک شی حاوی خاصیت گروه‌ها را با محتوا مانند { id: '1' } برای مسیری شبیه به /products/1 باز می‌گرداند. بیایید چنین کاری را نشان دهیم:

// url-to-regex.js

function parse(url) {
  let str = "";

  for (var i =0; i < url.length; i++) {
    const c = url.charAt(i);
    if (c === ":") {
      // eat all characters
      let param = "";
      for (var j = i + 1; j < url.length; j++) {
        if (/\w/.test(url.charAt(j))) {
          param += url.charAt(j);
        } else {
          break;
        }
      }
      str += `(?<${param}>\\w+)`;
      i = j -1;
    } else {
      str += c;
    }
  }
  return str;
}

module.exports = parse;

و از آن استفاده کنیم:

const parse = require('./url-to-regex');

const regex = parse("/products/:id")).toBe("/products/(?<id>\\w+)");
const match = "/products/114".match(new RegExp(regex);
// match.groups is { id: '114' } 

افزودن مسیریابی به سرور

بیایید دوباره فایل server.js را باز کنیم و قسمت مدیریت مسیر را اضافه کنیم.

// server.js
const http = require('http')
const parse = require('./regex-from-url')

function myServer() {
  let routeTable = {};
  http.createServer((req, res) => {
    const routes = Object.keys(routeTable);
    let match = false;
    for(var i =0; i < routes.length; i++) {
       const route = routes[i];
       const parsedRoute = parse(route);
       if (
         new RegExp(parsedRoute).test(req.url) &&
         routeTable[route][req.method.toLowerCase()]
       ) {
         let cb = routeTable[route][req.method.toLowerCase()];

         const m = req.url.match(new RegExp(parsedRoute));

         req.params = m.groups;

         cb(req, res);

         match = true;
         break;
       }
    }
    if (!match) {
      res.statusCode = 404;
      res.end("Not found");
    }
  });   

  return {
    get(path, cb) {
      routeTable[path] = { 'get': cb }
    }
  }
}

کاری که ما انجام می‌دهیم، حلقه گذاشتن تمام مسیرهای موجود است. مقایسه این چنین به نظر می‌رسد:

if (
  new RegExp(parsedRoute).test(req.url) &&
  routeTable[route][req.method.toLowerCase()]
)

همچنین توجه داشته باشید که چگونه پارامترهای مسیر تجزیه می‌شوند و در ویژگی پارامترها قرار می‌گیرند:

const m = req.url.match(new RegExp(parsedRoute));
req.params = m.groups;

پارامترهای کوئری

ما از قبل می‌دانیم که با استفاده از ماژول HTTP ، URL شامل مسیر ما خواهد بود، مانند products?page=1&pageSize/. مرحله بعدی کشف این پارامترها است. این امر می‌تواند با استفاده از کد RegEx مانند کد زیر انجام شود:

/ query-params.js

function parse(url) {
  const results = url.match(/\?(?<query>.*)/);
  if (!results) {
    return {};
  }
  const { groups: { query } } = results;

  const pairs = query.match(/(?<param>\w+)=(?<value>\w+)/g);
  const params = pairs.reduce((acc, curr) => {
    const [key, value] = curr.split(("="));
    acc[key] = value;
    return acc;
  }, {});
  return params;
}

module.exports = parse;

اکنون باید آن را به کد سرور وصل کنیم. خوشبختانه این فقط چند سطر است:

const queryParse = require('./query-params.js')

// the rest omitted for brevity
ress.query = queryParse(req.url);

ارسال داده از body

خواندن body را می‌توان با فهم اینکه req پارامتر ورودی از نوع استریم است انجام داد. خوب است بدانید که داده‌ها به قطعات کوچک، به اصطلاح تکه‌ها می‌رسند. با گوش دادن به پایان رویداد، مشتری اجازه داده است اکنون انتقال کامل شود و داده دیگری ارسال نمی‌شود.

با گوش دادن به داده‌های رویداد، می‌توانید به داده‌های ورودی گوش دهید، مانند زیر:

req.on('data', (chunk) => {
  // do something
})

req.on('end', () => {
  // no more data
})

بنابراین، برای گوش دادن به داده‌های انتقال یافته از کلاینت، می‌توانیم روش کمکی زیر را ایجاد کنیم:

function readBody(req) {
    return new Promise((resolve, reject) => {
      let body = "";
      req.on("data", (chunk) => {
        body += "" + chunk;
      });
      req.on("end", () => {
        resolve(body);
      });
      req.on("error", (err) => {
        reject(err);
      });
    });
  }

و سپس از آن در کد سرور استفاده کنید:

res.body = await readBody(req);

کد کامل در این مرحله باید به صورت زیر باشد:

// server.js

const http = require('http')
const queryParse = require('./query-params.js')
const parse = require('./regex-from-url')

function readBody(req) {
    return new Promise((resolve, reject) => {
      let body = "";
      req.on("data", (chunk) => {
        body += "" + chunk;
      });
      req.on("end", () => {
        resolve(body);
      });
      req.on("error", (err) => {
        reject(err);
      });
    });
  }

function myServer() {
  let routeTable = {};
  http.createServer(async(req, res) => {
    const routes = Object.keys(routeTable);
    let match = false;
    for(var i =0; i < routes.length; i++) {
       const route = routes[i];
       const parsedRoute = parse(route);
       if (
         new RegExp(parsedRoute).test(req.url) &&
         routeTable[route][req.method.toLowerCase()]
       ) {
         let cb = routeTable[route][req.method.toLowerCase()];

         const m = req.url.match(new RegExp(parsedRoute));

         req.params = m.groups;
         req.query = queryParse(req.url);
         req.body = await readBody(req);

         cb(req, res);

         match = true;
         break;
       }
    }
    if (!match) {
      res.statusCode = 404;
      res.end("Not found");
    }
  });   

  return {
    get(path, cb) {
      routeTable[path] = { 'get': cb }
    },
    post(path, cb) {
      routeTable[path] = { 'post': cb }
    }
  }
}

در این مرحله باید بتوانید کد خود را به این صورت فراخوانی کنید:

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

app.get('/products/:id', (req, res) => {
  // for route /products/1, req.params has value  { id: '1' }

})
app.get('/products/', (req, res) => {
  // for route /products?page=1&pageSize=10, req.query has value  { page: '1', pageSize: '10' }
})
app.post('/products/', (req, res) => {
  // req.body should contain whatever you sent across as client
})

کمک کننده‌های پاسخ

در این مرحله کار زیادی انجام می‌شود. اما چگونه می‌توانید داده‌ها را به کلاینت برگردانید؟ از آنجا که شما در حال اجرای ماژول HTTP هستید، می‌توانید از پارامتر res استفاده کنید. با فراخوانی end() می‌توانید داده‌ها را به عقب برگردانید. در اینجا مثالی آورده شده است:

res.end('some data')

با این حال، اگر نگاه کنید که Express چگونه این کار را انجام می‌دهد، انواع کمک کننده برای این موارد مانند send () ، json () ، html() و ... را دارد. شما می‌توانید آن را با چند خط کد نیز داشته باشید:

function createResponse(res) {
  res.send = (message) => res.end(message);
  res.json = (message) => {
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify(message));
  };
  res.html = (message) => {
    res.setHeader("Content-Type", "text/html");
    res.end(message); 
  }
  return res;
}

و مطمئن شوید که آن را در کد سرور اضافه کرده‌اید:

res = createResponse(res);

Middleware

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

server.get("/protected", (req, res, next) => {
  if (req.headers["authorization"] === "abc123") {
    next();
  } else {
    res.statusCode = 401;
    res.send("Not allowed");
  }
 }, (req, res) => {
   res.send("protected route");
 });

استدلال دوم میان‌افزار است. این گزینه req.headers را برای یک مجوز بررسی می‌کند و مقدار آن را برمی‌گرداند. اگر همه چیز خوب باشد، بعد از next() فراخوانی می‌شود. اگر خوب نباشد، درخواست در اینجا متوقف شده و res.send() فراخوانی می‌شود و کد وضعیت 401 تعیین می‌شود و بیان می‌کند که مجاز نیست.

آخرین بحث، پاسخ مسیری است که شما می‌خواهید کاربر آن را مشاهده کند، به شرطی که مقدار هدر را برای شما ارسال کند.

بیایید این را پیاده‌سازی کنیم. تابع زیر را در server.js ایجاد کنید:

function processMiddleware(middleware, req, res) {
  if (!middleware) {
    // resolve false
    return new Promise((resolve) => resolve(true));
  }

  return new Promise((resolve) => {
    middleware(req, res, function () {
      resolve(true);
    });
  });
}

بالاتر از پارامتر میان‌افزار فراخوانی شده است و می‌بینید که آخرین آرگومان چگونه تابعی است که می‌تواند آن را حل کند:

middleware(req, res, function () {
  resolve(true);
});

برای استفاده از کد سرور این مراحل، چند مرحله دیگر وجود دارد که باید انجام دهیم:

  1. اطمینان حاصل کنید که میان‌افزار ثبت شده باشد
  2. هنگامی که درخواست match داریم، از میان‌افزار استفاده کنید
  3. میان‌افزار را فراخوانی کنید

ثبت کردن میان‌افزار

باید با افزودن اولین متد کمک کننده، نحوه ثبت‌نام مسیرها را کمی تغییر دهیم:

function registerPath(path, cb, method, middleware) {
    if (!routeTable[path]) {
      routeTable[path] = {};
    } 
    routeTable[path] = { ...routeTable[path], [method]: cb, [method + "-middleware"]: middleware };
  }

بنابراین تلاش می‌کنیم مسیری مانند زیر را ثبت کنیم:

server.get('/products', (req, res, next) => {}, (req, res) => {})

که به میان‌افزار اجازه می‌دهد بر اساس خصوصیت get فراخوانی را انجام دهد.

سپس وقتی مسیر را ثبت می‌کنیم، به جای آن کار زیر را انجام می‌دهیم:

return {
    get: (path, ...rest) => {
      if (rest.length === 1) {
        registerPath(path, rest[0] , "get");
      } else {
        registerPath(path, rest[1], "get", rest[0]);
      }
    },

دریافت یک مرجع برای میان‌افزار 

برای دریافت مرجع به میان‌افزار، می‌توانیم از این کد استفاده کنیم:

let middleware = routeTable[route][`${req.method.toLowerCase()}-middleware`];

فرایند میان‌افزار

const result = await processMiddleware(middleware, req, createResponse(res));
if (result) {
  cb(req, res);
}

خلاصه

کد کامل در لینک زیر موجود است:

https://github.com/softchris/mini-web

و همچنین می‌توانید از طریق NPM از آن استفاده کنید:

npm install quarkhttp

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

منبع

گردآوری و تالیف عرفان حشمتی
آفلاین
user-avatar

عرفان حشمتی هستم، مهندس سخت افزار و برنامه نویس و طراح وب سایت، علاقه مند به دنیای آی تی و تکنولوژی، همچنین در حوزه ادیت فیلم و تصویر مطالعه و تمرین می کنم.

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید