فصل 20Node.js

دانش‌آموزی پرسید، 'برنامه‌نویس‌های قدیم فقط با کامپیوترهای ابتدایی و بدون استفاده از زبان‌های برنامه‌نویسی، توانستند برنامه‌های زیبایی بنویسند. چرا ما از کامپیوترهای پیشرفته و زبان‌های برنامه نویسی استفاده می‌کنیم؟ '. Fu-Tzu پاسخ داد، معمار‌های قدیم هم فقط از خشت و چوب استفاده می‌کردند، البته کلبه‌های زیبایی هم می‌ساختند.’

استاد Yuan-Ma, کتاب برنامه‌نویسی
Picture of a telephone pole

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

هدف این دو فصل آموزش مفاهیم اصلی Node.js است؛ تا برای نوشتن برنامه‌های Node.js اطلاعات کافی در اختیار داشته باشید. طبیعتا محتوای آن‌ها کامل و جامع نیست.

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

اگر می خواهید همراه با این فصل کد‌ها را نیز امتحان کنید، لازم است تا Node.js نسخه‌ی 10.1 یا بالاتر را نصب کنید. برای این کار به آدرس https://nodejs.org بروید و طبق دستور راهنما، آن را روی سیستم عامل تان نصب کنید. همچنین در این آدرس مستندات بیشتری برای Node.js موجود است.

پیش‌زمینه

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

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

Node در ابتدا به منظور آسان‌سازی برنامه‌نویسی ناهمگام ابداع شد. جاوااسکریپت بسیار مناسب سیستمی مانند Node است. جاوااسکریپت یکی از معدود زبان‌های برنامه‌نویسی است که به صورت درونی و ذاتی راهی برای پشتیبانی از عملیات ورودی/خروجی ندارد. بنابراین، می توانست گزینه‌ی مناسبی برای رویکرد نامتعارف Node در مدیریت I/O باشد بدون اینکه در انتها شاهد وجود دو رابط ناسازگار باشیم. در سال 2009، زمانی که Node در حال طراحی شدن بود، برنامه‌نویسی مبتنی بر callback در مرورگر مرسوم بود، بنابراین جامعه‌ی فعال در جاوااسکریپت به سبک برنامه‌نویسی ناهمگام آشنا بودند.

دستور node

پس از نصب Node.js روی یک سیستم، برنامه‌ای به نام node در دسترس خواهد بود، که برای اجرای فایل‌های جاوااسکریپت استفاده می شود. فرض کنید فایلی به نام hello.js دارید، که کد زیر را دارد:

let message = "Hello world";
console.log(message);

می توانید node را از خط فرمان مانند مثال زیر برای اجرای برنامه استفاده کنید.

$ node hello.js
Hello world

متد console.log در محیط Node، کاربردی شبیه به عملکردش در مرورگر دارد. چاپ یک رشته‌ی متنی. اما در Node، این متن به جای اینکه به کنسول جاوااسکریپت مرورگر داده شود، به استریم خروجی استاندارد پروسه‌ (process) ارسال می‌شود . در هنگام اجرای node از طریق خط فرمان، گزارش‌های تولیدی توسط این متد را می‌توانید در ترمینال خط‌ فرمانتان مشاهده کنید.

اگر دستور node را بدون مشخص نمودن یک فایل اجرا کنید، به شما فضایی برای نوشتن کد جاوااسکریپت خواهد داد، تا بتوانید نتیجه را بلافاصله مشاهده کنید.

$ node
> 1 + 1
2
> [-1, -2, -3].map(Math.abs)
[1, 2, 3]
> process.exit(0)
$

متغیر process درست مثل console، به صورت سراسری در Node در دسترس است. این متغیر روش‌هایی برای بررسی و دستکاری برنامه‌ی فعلی را در اختیارتان می‌گذارد. متد exit باعث به اتمام رساندن پروسه‌ی فعلی می‌شود که یک کد وضعیت خروج را نیز می‌تواند دریافت کند؛ کدی که به برنامه‌ی اجرا‌کننده‌ی node (در اینجا پوسته‌ی خط فرمان) اطلاع می‌دهد که برنامه‌ی مورد نظر با موفقیت به اتمام رسیده است (کد صفر ) یا با خطایی روبرو شده است (هر کد دیگری).

برای دستیابی به آرگومان‌هایی که توسط خط فرمان به اسکریپت شما داده‌ می‌شود، می توانید ‍process.argv را بخوانید، که آرایه‌ای از رشته‌ است. توجه داشته باشید که نام اسکریپت شما و دستور node‍ را نیز در بر دارد. بنابراین آرگومان‌های مورد نظر از خانه‌ی 2 آرایه شروع می شوند. اگر showargv.js حاوی دستور
console.log(process.argv) باشد، می‌توانید به شکل زیر آن را اجرا نمایید:

$ node showargv.js one --and two
["node", "/tmp/showargv.js", "one", "--and", "two"]

تمامی متغیر‌های سراسری جاوااسکریپت استاندارد، مانند Array، Math، و JSON، همه در محیط Node نیز در دسترس هستند. اما آن موارد مخصوص به مروگر مثل document یا prompt طبیعتا وجود ندارند.

ماژول‌ها

Node علاوه بر متغیرهایی که معرفی شد، مانند console و process، چند متغیر دیگر را نیز در فضای سراسری قرار داده است. اگر بخواهید به امکانات و ویژگی‌های درونی دسترسی داشته باشید، باید از سیستم ماژول Node کمک بگیرید.

سیستم ماژول CommonJS، که بر اساس تابع require می‌باشد، در فصل 10 توصیف شد. این سیستم به صورت درونی در Node قرار داده شده است و برای بارگیری ماژول‌ها چه درونی چه بارگیری شده به صورت بسته و همچنین فایل‌های موجود در برنامه‌ی شما استفاده می ‌شود.

زمانی که تابع require فراخوانی می‌شود، Node باید رشته‌ی داده‌شده را به فایلی که بتوان بارگیری کرد تفسیر کند. مسیر‌هایی که با / ، ./ یا ../ شروع می شوند با توجه به مسیر ماژول فعلی به صورت نسبی تفسیر می شوند. که . نماینده‌ی پوشه‌ی فعلی، ../ به معنای یک پوشه بالاتر و / به معنای root یا ریشه‌ در سیستم فایل می‌باشد. بنابراین اگر درخواست "./graph" را از درون /tmp/robot/robot.js داشته باشید، Node به دنبال بارگیری /tmp/robot/graph.js خواهد بود.

می توان از نوشتن پسوند .js صرف نظر کرد، زیرا Node در صورت وجود فایل مورد نظر، آن را خود در نظر خواهد گرفت. اگر مسیر خواسته شده به یک پوشه اشاره کند، Node سعی می کند تا فایلی به نام index.js را از آن پوشه بارگیری کند.

زمانی که یک رشته‌ی متنی که شباهتی به یک مسیر نسبی یا مطلق ندارد به تابع require داده می‌شود، فرض بر آن است که یا یک ماژول درونی، مورد نظر است یا ماژولی که در پوشه‌ی node_modules نصب شده است. به عنوان مثال، require("fs") به شما ماژول مدیریت سیستم فایل درونی Node را خواهد داد. و require("robot") نیز سعی خواهد کرد تا بسته‌ای که در node_modules/robot/ وجود دارد را بارگیری کند. روش رایج نصب این گونه‌ بسته‌ها یا کتابخانه‌ها، استفاده از NPM است که به زودی آن را توضیح خواهیم داد.

بیایید یک پروژه‌ی کوچک شامل دو فایل ایجاد کیم. فایل اول main.js است که اسکریپتی است که می توان آن را از خط فرمان اجرا کرد و رشته‌ای را وارونه می کند.

const {reverse} = require("./reverse");

// Index 2 holds the first actual command line argument
let argument = process.argv[2];

console.log(reverse(argument));

فایل بعدی reverse.js یک کتابخانه برای وارونه‌کردن رشته‌ها تعریف می کند، که می توان از آن هم برای این ابزار خط فرمان بهره برد و هم دیگر اسکریپت‌هایی که نیاز به دسترسی مستقیم به تابعی برای وارونه‌سازی رشته‌ها دارند.

exports.reverse = function(string) {
  return Array.from(string).reverse().join("");
};

به خاطر داشته باشید که افزودن خاصیت‌ها به exports باعث می‌شود که آن‌ها به رابط ماژول اضافه شوند. به دلیل اینکه Node.js فایل‌ها را به عنوان ماژول‌های CommonJs در نظر می‌گیرد، main.js می تواند تابع صادر شده‌ی (exported) reverse را از فایل reverse.js بردارد.

اکنون می توانیم ابزارمان را به شکل زیر فراخوانی کنیم:

$ node main.js JavaScript
tpircSavaJ

نصب به وسیله‌ی NPM

NPM، که درفصل 10 معرفی شد، مخزنی آنلاین از ماژول‌های جاوااسکریپت است که خیلی از آن‌ها به طور خاص برای Node نوشته شده اند. زمانی که Node را روی کامپیوترتان نصب می کنید، دستور npm نیز در دسترس شما قرار می‌گیرد که به کمک آن می توانید با این مخزن تعامل کنید.

کاربرد اصلی NPM بارگیری بسته‌ها می باشد. با بسته‌ی ini در فصل 10 آشنا شدیم. می توانیم با استفاده از NPM آن را دریافت و روی کامپیوترمان نصب کنیم.

$ npm install ini
npm WARN enoent ENOENT: no such file or directory,
         open '/tmp/package.json'
+ ini@1.3.5
added 1 package in 0.552s

$ node
> const {parse} = require("ini");
> parse("x = 1\ny = 2");
{ x: '1', y: '2' }

پس از اجرای npm install، برنامه‌ی NPM یک پوشه به نام node_modules ایجاد خواهد کرد. درون آن پوشه، پوشه‌ای دیگر به نام ini خواهد بود که حاوی کتابخانه‌ی مورد نظر می باشد. می‌توانید آن را باز کنید و نگاهی به کدهایش بیاندازید. زمانی که require("ini") را فراخوانی می‌کنیم، این کتابخانه بارگیری می شود و می‌توانیم خاصیت parse آن را فراخوانی کنیم تا فایل تنظیمات ما خوانده شود.

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

فایل‌های بسته

در مثال npm install، یه پیام هشدار در مورد نبود فایل ‍package.json را مشاهده کردید. ساخت این فایل برای هر پروژه توصیه می‌شود، چه به صورت دستی چه با استفاده از دستور npm init. این فایل حاوی اطلاعاتی درباره‌ی پروژه می باشد مانند نام پروژه، نسخه‌ی آن و لیست وابستگی‌های آن.

شبیه‌سازی ربات از فصل فصل 7 ، که در تمرین مربوط به فصل فصل 10 ماژولار شد، می تواند یک فایل ‍package.json شبیه این داشته باشد:

{
  "author": "Marijn Haverbeke",
  "name": "eloquent-javascript-robot",
  "description": "Simulation of a package-delivery robot",
  "version": "1.0.0",
  "main": "run.js",
  "dependencies": {
    "dijkstrajs": "^1.0.1",
    "random-item": "^1.0.0"
  },
  "license": "ISC"
}

زمانی که دستور npm install را بدون نام بردن یک بسته برای نصب اجرا می کنید، NPM به سراغ لیست وابستگی‌های موجود در package.json‍ می رود. زمانی که یک بسته‌ی مشخص را نصب می کنید که پیش از این به عنوان یک وابستگی لیست نشده است، NPM آن را به package.json اضافه می کند.

نسخه‌ها

یک فایل package.json هم نسخه‌ی خود برنامه را نگه‌داری می کند هم نسخه‌های وابستگی‌هایش را. نسخه‌ها روشی‌ هستند که می توان به وسیله‌ی آن‌ها، با این واقعیت که بسته‌ها جداگانه تغییر می‌کنند و کدی که مثلا نوشته شده تا با بسته‌ای کار کند، ممکن است با نسخه‌ی بعدی آن یا نسخه‌ای ویرایش شده از آن کار نکند.

بر طبق NPM بسته‌ها باید از شمایی موسوم به semantic versioning یا نسخه‌بندی معنایی پیروی کنند، که اطلاعاتی در مورد نسخه‌هایی که سازگار هستند (به این معنا که رابط قبلی را پشتیبانی می‌کنند) را در شماره‌ی نسخه لحاظ می کند. یک نسخه‌ی معنایی از سه عدد تشکیل می‌شود، که هر کدام با نقطه جدا می شوند، مانند 2.3.0. هربار که یک ویژگی جدید به بسته اضافه می‌شود، به عدد میانی باید افزوده شود. هربار که سازگاری شکسته شود، که در این صورت کد موجود ممکن است با این نسخه دیگر کار نکند، عدد ابتدایی باید تغییر یابد.

کاراکتر (^) در ابتدای عدد نسخه برای یک وابستگی در package.json، به این معنا است که هر نسخه‌ای که با عدد داده شده سازگار است می تواند نصب شود. بنابراین، به عنوان مثال، "^2.3.0" به این معنا است که هر نسخه‌ای بالاتر یا برابر با 2.3.0 و پایین تر از 3.0.0 قابل قبول است.

دستور npm همچنین برای انتشار بسته‌های جدید یا نسخه‌های جدید بسته‌ها استفاده می‌شود. اگر دستور npm publish را در یک پوشه‌ی حاوی فایل package.json اجرا کنید، باعث می شود بسته‌ای با نام و شماره‌ نسخه‌ی موجود در فایل JSON در مخزن منتشر شود. هر کسی می‌تواند در NPM بسته منتشر کند البته که نام بسته نباید قبلا استفاده شده باشد.

با توجه به اینکه برنامه‌ی npm نرم‌افزاری است که با یک سیستم باز تعامل دارد - مخزن بسته‌ها - کاری که می‌کند منحصر به فرد نیست. برنامه‌ای دیگر که yarn نام دارد، نیز می‌تواند از طریق NPM نصب شود که کاری شبیه به ‍npm انجام می دهد ولی با دستورات و استراتژی متفاوت.

این کتاب به سراغ جزئیات بیشتر مربوط به NPM نمی‌رود. برای اطلاعات بیشتر و جستجوی بسته‌ها به https://npmjs.org مراجعه کنید.

ماژول سیستم فایل

یکی از پراستفاده‌ترین ماژول‌های درونی در Node، ماژول fs می‌باشد، که سرنام file system است. این ماژول توابعی را برای کار با فایل‌ها و پوشه‌ها فراهم می‌سازد.

به عنوان مثال، تابع readFile فایلی را می‌خواند و سپس یک تابع callback را با محتوای فایل خوانده شده فراخوانی می کند.

let {readFile} = require("fs");
readFile("file.txt", "utf8", (error, text) => {
  if (error) throw error;
  console.log("The file contains:", text);
});

ورودی دوم تابع readFile مشخص کننده‌ی کدبندی کاراکتر (character encoding) است که برای رمزگشایی و تبدیل محتوای فایل به رشته استفاده می‌شود. راه‌های متعددی برای کدگذاری متون به داده‌های باینری وجود دارد، اما بیشتر سیستم‌های مدرن از UTF-8 استفاده می کنند. بنابراین اگر دلیلی برای استفاده از یک کدبندی دیگر ندارید، همان "utf8" را در هنگام خواندن یک فایل استفاده کنید. اگر کدبندی را به تابع ارسال نکنید، Node بنا را بر این می‌گذارد که شما به محتوای دودویی علاقمند هستید و یک شیء Buffer به جای رشته‌ی متنی برمی‌گرداند که شیئی آرایه‌گونه است و اعدادی دارد که نمایانگر بایت‌ها در فایل‌ها می‌باشند (قطعات 8 بیتی داده) .

const {readFile} = require("fs");
readFile("file.txt", (error, buffer) => {
  if (error) throw error;
  console.log("The file contained", buffer.length, "bytes.",
              "The first byte is:", buffer[0]);
});

تابعی مشابه به نام writeFile وجود دارد که برای نوشتن یک فایل روی دیسک استفاده می شود.

const {writeFile} = require("fs");
writeFile("graffiti.txt", "Node was here", err => {
  if (err) console.log(`Failed to write file: ${err}`);
  else console.log("File written.");
});

در اینجا لازم نبود تا کدبندی مشخص شود-writeFile زمانی که به جای Buffer‍، یک رشته دریافت می کند، آن را به صورت رشته و با کدبندی پیش‌فرض که همان UTF-8 است، می نویسد.

ماژول fs‍، توابع مفید دیگری نیز دارد: readdire فایل‌های موجود در یک پوشه‌ را به صورت یک آرایه‌ی رشته‌ای برمی‌گرداند، stat اطلاعاتی در درباره‌ی یک فایل برمی‌گرداند، rename نام یک فایل را تغییر می‌دهد، unlink فایلی را حذف می کند و الی آخر. می توانید مستندات آن را در https://nodejs.org مشاهده کنید.

بیشتر این توابع یک تابع callback به عنوان آخرین ورودی دریافت می کنند، که یا با یک error (آرگومان اول) یا با یک نتیجه‌ی موفق (آرگومان دوم) آن‌ را فراخوانی‌ می کنند. همانطور که در فصل 11 دیدیم، این سبک از برنامه‌نویسی ایراداتی دارد که بزرگترین آن مدیریت خطاها‌ است که شلوغ و دردسرساز می شود.

اگرچه promise ها مدتی است که بخشی از جاوااسکریپت هستند، اما در زمان نوشتن این کتاب، پیاده‌سازی آن‌ها هنوز در جریان است. شیئی به نام promises توسط بسته‌ی fs از نسخه‌ی 10.1 صادر می‌شود که همان توابع موجود در fs را دارد با این تفاوت که از promiseها به جای توابع callback استفاده می کنند.

const {readFile} = require("fs").promises;
readFile("file.txt", "utf8")
  .then(text => console.log("The file contains:", text));

گاهی نیازی به ناهمگامی ندارید و فقط با توجه به پیش‌فرض بودن آن‌ها مورد استفاده قرار می‌گیرند . خیلی از توابع موجود در fs یک نسخه‌ی همگام نیز دارند که نامشان یکسان است با این تفاوت که Sync به انتهایشان اضافه شده است. به عنوان مثال، نسخه‌ی همگام readFile به صورت readFileSync در دسترس است.

const {readFileSync} = require("fs");
console.log("The file contains:",
            readFileSync("file.txt", "utf8"));

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

ماژول HTTP

یکی دیگر از ماژول‌های اصلی http است. این ماژول امکاناتی برای اجرای سرویس‌دهنده‌های HTTP و ساختن درخواست‌های HTTP فراهم می کند.

تمام آنچه برای راه‌اندازی یک سرویس‌دهنده‌ی HTTP لازم است:

const {createServer} = require("http");
let server = createServer((request, response) => {
  response.writeHead(200, {"Content-Type": "text/html"});
  response.write(`
    <h1>Hello!</h1>
    <p>You asked for <code>${request.url}</code></p>`);
  response.end();
});
server.listen(8000);
console.log("Listening! (port 8000)");

اگر این اسکریپت را در کامپیوتر خودتان اجرا کنید، می‌توانید در مروگر آدرس http://localhost:8000/hello را باز کنید تا یک درخواست به سرویس‌دهنده‌تان ارسال کنید. پاسخ سرویس‌دهنده یک صفحه‌ی ساده‌ی HTML خواهد بود.

تابعی که به عنوان آرگومان به createServer داده‌ می‌شود هر بار که کلاینت (سرویس‌گیرنده) به سرویس‌دهنده متصل می شود، فراخوانی می شود. دو متغیر request و response اشیائی هستند که نمایانگر داده‌های ورودی و برگشتی می‌باشند. شیء اول دربردارنده‌ی اطلاعاتی درباره‌ی درخواست است مانند url درخواست، که مشخص می‌کند درخواست به کدام URL ارسال شده است.

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

برای اینکه چیزی برگردانید، متدهایی روی شیء response فراخوانی می کنید. اولین متد، متد writeHead است که سرنام‌های پاسخ یا headers را می‌نویسد، به فصل 18 مراجعه کنید. به این متد یک کد وضعیت (مثل 200 یا “OK” در این مورد) و یک شیء حاوی مقادیر سرنام‌ها داده می‌شود. در مثال، سرنام Content-Type تنظیم می‌شود تا کلاینت باخبر شود که ما قرار است یک سند HTML برگردانیم.

سپس، بدنه‌ی اصلی پاسخ (که محتوای سند است) به وسیله‌ی response.write ارسال می‌شود. می توانید این متد را چندین بار فراخوانی کنید اگر دوست دارید تا پاسخ را بخش بخش ارسال کنید، مثلا استریم کردن داده ها به کلاینت همزمان با آماده شدن آن‌ها. در آخر، response.end پایان پاسخ را مشخص می کند.

فراخوانی server.listen باعث می‌شود که سرویس‌دهنده منتظر ارتباطات روی پورت 8000 بماند. به همین علت است که شما باید به localhost:8000 متصل شوید تا بتوانید با سرویس‌دهنده تعامل کنید زیرا localhost به صورت پیش‌فرض از پورت 80 استفاده می کند.

هنگامی که این اسکریپت را اجرا می کنید، پروسه‌ی مورد نظر اجرا و منتظر درخواست‌ها می ماند. زمانی که یک اسکریپت منتظر رخداد‌ها می‌باشد-در این مثال، ارتباطات شبکه- node به صورت خودکار پس از رسیدن به پایان اسکریپت متوقف نمی‌شود. برای متوقف کردن آن باید کلید‌های control-C را فشار دهید.

یک سرویس‌دهنده‌ی وب واقعی معمولا کارهای بیشتری از آنچه در مثال نشان‌داده شد انجام می دهد- به متد درخواست توجه می کند (خاصیت method) تا متوجه منظور کلاینت از ارسال درخواست بشود و به URL درخواست نگاه می کند تا منبع مورد نظر این درخواست برای اعمال را بداند. در ادامه‌ی این فصل، یک سرویس‌دهنده‌ی پیش‌رفته‌تر را مشاهده خواهیم کرد.

برای اینکه مانند یک کلاینت HTTP عمل کنیم، می توانیم از تابع request ماژول http استفاده کنیم.

const {request} = require("http");
let requestStream = request({
  hostname: "eloquentjavascript.net",
  path: "/20_node.html",
  method: "GET",
  headers: {Accept: "text/html"}
}, response => {
  console.log("Server responded with status code",
              response.statusCode);
});
requestStream.end();

آرگومان اول request، درخواست را تنظیم می کند، مشخص می کند به کدام سرویس‌دهنده باید تماس حاصل شود، چه مسیری از آن سرویس‌دهنده مورد درخواست قرار بگیرد، چه متدی استفاده شود و از این قبیل. آرگومان دوم تابعی است که زمانی باید فراخوانی شود که پاسخ از راه می‌رسد. به این تابع شیئ داده‌ می‌شود که به ما امکان بررسی محتوای پاسخ را می‌دهد، مثلا دریافت کد وضعیت.

درست شبیه به response که پیش‌تر در سرویس‌دهنده‌ دیدیم، شیئی که توسط request برگردانده‌ می‌شود به ما این امکان را می‌دهد تا داده‌ها را به وسیله‌ی متد write درون request استریم کنیم و به وسیله‌ی متد end آن، آن را به پایان برسانیم. در مثال، از متد write استفاده نشده است زیرا درخواست‌های نوع GET نباید داده‌ای در بدنه‌ی درخواست داشته باشند.

در ماژول https تابع مشابهی به نام request وجود دارد که می‌تواند برای درخواست‌هایی که به URL‌های https: می باشند استفاده شود.

ساخت درخواست‌ها به وسیله‌ی امکانات خام Node، نسبتا زیادنویسی است. در NPM بسته‌های پوششی بسیار ساده‌تر برای این کار وجود دارد. به عنوان مثال، بسته‌ی node-fetch رابط fetch را به صورت مبتنی بر promise ها فراهم می‌سازد که ما با آن در موضوع مرورگرها آشنا شدیم.

استریم یا جریان

دو نمونه از استریم‌های قابل نوشتن را در مثال‌های HTTP مشاهده کرده ایم: شیء پاسخ که سرویس‌دهنده می توانست در آن بنویسد و شیء درخواستی که از request برگردانده می‌شد.

استریم‌های قابل نوشتن (Writable streams) مفهومی پراستفاده در Node می‌باشند. این‌گونه اشیاء متدی به نام write دارند که یک رشته یا یک Buffer برای نوشتن چیزی در استریم دریافت می کند. متد end‍ ‌آن‌ها، باعث می‌شود که استریم بسته شود و می‌توان به صورت اختیاری مقداری را به آن داد که قبل از بستن استریم آن را در آن بنویسید. هر دوی این متد‌ها همچنین یک تابع callback به عنوان آرگومان اضافی قبول می‌کنند که آن را هنگامی که عمل نوشتن یا بستن پایان پذیرفت، فراخوانی می کنند.

همچنین این امکان وجود دارد که به وسیله‌ی تابع createWriteStream ماژول fs یک استریم قابل نوشتن ایجاد کرد که به یک فایل اشاره می کند. سپس می‌توانید از متد write موجود در شیء حاصل استفاده کنید تا فایل را به‌جای نوشتن در یک حرکت با writeFile، به صورت گام به گام بنویسید.

استریم‌های قابل‌خواندن اندکی پیچیده‌تر هستند. هر دوی متغیرهای request و ‍response که به callback مربوط به کلاینت HTTP داده می‌شوند، استریم‌های قابل‌خواندن هستند- یک سرویس‌دهنده درخواست‌ها را می‌خواند و سپس پاسخ‌ها را می‌نویسد، درحالی‌که که کلاینت ابتدا یک درخواست می‌نویسد و سپس یک پاسخ را می‌خواند. خواندن از یک استریم به وسیله‌ی استفاده از گرداننده‌های رخداد صورت می‌گیرد نه متد‌ها.

در Node، اشیائی که رخداد‌ها را تولید‌ می‌کنند متدی به نام on دارند که شبیه به متد addEventListener موجود در مرورگر است. نام رخداد و یک تابع به آن می‌دهید، و این متد تابع دریافتی را ثبت کرده و هنگامی که رخداد داده شده اتفاق افتاد، آن را فراخوانی می‌کند.

استریم‌های قابل خواندن دارای رخداد‌های "data" و "end" می‌باشند. اولین رخداد با هربار ورود داده ایجاد می‌شود، و دومین هنگامی که استریم به پایان می‌رسد فراخوانی می‌شود. این مدل بسیار مناسب داده‌های جریان‌داری (streaming) که می‌توانند بلافاصله پردازش شوند می‌باشد، حتی هنگامی که کل سند هنوز در دسترس نیست. یک فایل را می‌توان با استفاده از تابع createReadStream ماژول fs به صورت یک استریم قابل‌خواندن خواند.

کد زیر یک سرویس‌دهنده ایجاد می‌کند که بدنه‌های درخواست را می‌خواند و آن‌ها را به کلاینت به صورت حروف بزرگ استریم می ‌کند:

const {createServer} = require("http");
createServer((request, response) => {
  response.writeHead(200, {"Content-Type": "text/plain"});
  request.on("data", chunk =>
    response.write(chunk.toString().toUpperCase()));
  request.on("end", () => response.end());
}).listen(8000);

مقدار chunk که به گرداننده‌ی data ارسال‌ می‌شود یک ‍‍Buffer دودویی خواهد بود. می‌توانیم این را با کدگشایی آن به عنوان کاراکتر‌های کدگذاری شده با UTF-8 به وسیله‌ی متد toString به یک رشته تبدیل کنیم.

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

const {request} = require("http");
request({
  hostname: "localhost",
  port: 8000,
  method: "POST"
}, response => {
  response.on("data", chunk =>
    process.stdout.write(chunk.toString()));
}).end("Hello server");
// → HELLO SERVER

در مثال عمل نوشتن در process.stdout (خروجی استاندارد process، که یک استریم قابل نوشتن است) صورت گرفته است؛ نه console.log. نمی‌توانیم از console.log استفاده کنیم زیرا این متد یک کاراکتر خط جدید بعد از هر بخش از متنی که می‌نویسد اضافه می‌کند، که مناسب اینجا نیست زیرا پاسخ ممکن است به صورت چند تکه دریافت شود.

یک سرویس‌دهنده‌ی فایل

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

زمانی که فایل‌ها را به عنوان منبع‌های HTTP در نظر می‌گیریم، متد‌های GET، PUT، و DELETE در HTTP را می‌توان برای خواندن، نوشتن و حذف فایل‌ها به کار برد. ما بخش مسیر در درخواست را به عنوان مسیر فایل مورد درخواست تفسیر خواهیم کرد.

احتمالا قصد نداریم کل سیستم فایلمان را به اشتراک بگذاریم، پس این مسیر‌ها را به عنوان شروع در پوشه‌ی کاری سرویس‌دهنده که همان پوشه‌ای است که در آن اجرا شده است، تفسیر می‌کنیم. اگر من سرویس‌دهنده را از /tmp/public/ (یا C:\tmp\public\ در ویندوز) اجرا کنم، سپس درخواستی برای فایل /file.txtارسال کنم، باید به /tmp/public/file.txt (یا C:\tmp\public\file.txt) اشاره شود.

برنامه‌ را قطعه به قطعه خواهیم نوشت، و از شیئی به نام methods برای ذخیره توابعی که متد‌های متنوع HTTP را مدیریت می‌کنند، استفاده خواهیم کرد. گرداننده‌های ما از نوع توابع async می‌باشند که شیء درخواست را به عنوان ورودی دریافت می کنند و یک promise برمی‌گردانند که منجر به یک شیء می‌شود که آن شیء توصیف کننده پاسخ می‌باشد.

const {createServer} = require("http");

const methods = Object.create(null);

createServer((request, response) => {
  let handler = methods[request.method] || notAllowed;
  handler(request)
    .catch(error => {
      if (error.status != null) return error;
      return {body: String(error), status: 500};
    })
    .then(({body, status = 200, type = "text/plain"}) => {
       response.writeHead(status, {"Content-Type": type});
       if (body && body.pipe) body.pipe(response);
       else response.end(body);
    });
}).listen(8000);

async function notAllowed(request) {
  return {
    status: 405,
    body: `Method ${request.method} not allowed.`
  };
}

این کد یک سرویس‌دهنده اجرا می‌کند که پاسخ‌های خطای 405 برمی‌گرداند، که این شماره در صورت عدم پذیرش متد داده شده توسط سرویس‌دهنده استفاده می‌شود.

زمانی که یک promise گرداننده‌ی درخواست لغو یا رد می‌شود، فراخوانی catch، خطای موجود را به یک شیء پاسخ تبدیل می‌کند، البته اگر خود به آن صورت نبود، بنابراین سرویس‌دهنده می‌تواند پاسخ خطا را بازگرداند و کلاینت را از ناموفق بودن درخواستش باخبر کند.

فیلد status در پاسخ ممکن است از قلم بیفتد، که در این صورت معادل 200 (OK) در نظر گرفته می‌شود. نوع محتوا (content type)، در خاصیت type را نیز می‌توان از قلم انداخت که در این صورت پاسخ به صورت رشته‌ی معمولی در نظر گرفته می‌شود.

هنگامی که مقدار body یک استریم قابل خواندن است، یک متد ‍‍pipe خواهد داشت که برای انتقال همه‌ی محتوا از یک استریم قابل خواندن به یک استریم قابل نوشتن استفاده می‌شود. اگر اینچنین نبود، فرض می‌شود که یا null (بدون بدنه)، یا رشته، یا بافر باشد و مستقیما به متد end پاسخ داده می‌شود.

برای اینکه مشخص کنیم کدام مسیر فایل به یک URL درخواست متناظر می‌شود، تابع urlPath از ماژول درونی url در Node بهره می‌برد تا URL را تجزیه کند. این تابع نام مسیررا که چیزی مثل "/file.txt" می‌باشد می‌گیرد، آن را کدگشایی می‌کند تا %20 یا کد‌های گریز را حذف کند، و آن را نسبت به پوشه‌ی فعال برنامه‌ نتیجه‌یابی کند.

const {parse} = require("url");
const {resolve, sep} = require("path");

const baseDirectory = process.cwd();

function urlPath(url) {
  let {pathname} = parse(url);
  let path = resolve(decodeURIComponent(pathname).slice(1));
  if (path != baseDirectory &&
      !path.startsWith(baseDirectory + sep)) {
    throw {status: 403, body: "Forbidden"};
  }
  return path;
}

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

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

برای پیشگیری از این گونه مشکلات، urlPath از تابع resolve متعلق به ماژول path بهره می‌برد، که مسیر‌های نسبی را واکاوی می‌کند. سپس تحقیق می‌کند که مسیر حاصل زیرمجموعه‌ی پوشه‌ی کاری برنامه باشد. تابع process.cwd (که cwd سرنام “current working directory” یا پوشه‌ی فعلی کاری می‌باشد) را می‌توان برای پیدا کردن پوشه‌ی کاری استفاده کرد. متغیر sep از بسته‌ی path، جداساز مسیر سیستم است- یک بک‌اسلش در ویندوز و اسلش در بیشتر دیگر سیستم‌ ها. زمانی که یک مسیر با پوشه‌ی پایه شروعی نمی‌شود، این تابع یک پاسخ خطا رها می‌کند، که به وسیله‌ی کد وضعیت HTTP مشخص می‌شود که دسترسی به این منبع ممنوع می‌باشد.

ما متد GET را برای دریافت لیستی از فایل‌ها در هنگام خواندن یک پوشه و نمایش محتوای فایل در صورت خواندن یک فایل، تنظیم می کنیم.

یک سوال مهم این است که چه نوع سرنام Content-Typeای باید در زمان بازگرداندن محتوای یک فایل انتخاب کنیم. با توجه به اینکه فایل‌های درخواستی می توانند از هر نوعی باشند، سرویس‌دهنده‌ی ما نمی‌تواند به سادگی فقط یک content type برای همه‌ی فایل ها در نظر بگیرد. دوباره NPM می تواند به ما کمک کند. بسته‌ی mime (نشان‌های نوع محتوا مانند text/plain که MIME types یا انواع MIME نیز خوانده می‌شوند.) نوع صحیح محتوا را برای تعداد زیادی از پسوند‌های فایل می‌داند.

دستور npm پیش‌رو، اگر در پوشه‌ای که اسکریپت سرویس‌دهنده قرار دارد اجرا شود، نسخه‌ی مشخص شده از mime را نصب می‌کند.

$ npm install mime@2.2.0

زمانی که فایلی درخواست می‌شود که وجود ندارد، کد وضعیت صحیحی که باید برگردانده شود کد 404 است. از تابع stat استفاده خواهیم کرد که به جستجوی اطلاعات در مورد فایل داده شده می‌پردازد و دو چیز را مشخص می‌کند: آیا فایل موجود است و اینکه آیا نتیجه پوشه است یا فایل.

const {createReadStream} = require("fs");
const {stat, readdir} = require("fs").promises;
const mime = require("mime");

methods.GET = async function(request) {
  let path = urlPath(request.url);
  let stats;
  try {
    stats = await stat(path);
  } catch (error) {
    if (error.code != "ENOENT") throw error;
    else return {status: 404, body: "File not found"};
  }
  if (stats.isDirectory()) {
    return {body: (await readdir(path)).join("\n")};
  } else {
    return {body: createReadStream(path),
            type: mime.getType(path)};
  }
};

به دلیل اینکه این تابع باید به سراغ دیسک برود پس ممکن است کمی زمان ببرد، ‍‍ stat ناهمگام است. با توجه به اینکه ما از سبک promiseها به جای callback استفاده می کنیم، باید آن را از promises وارد (import) نمود نه از ماژول fs;

هنگامی که فایل مورد نظر وجود ندارد، stat شیء خطایی رها می‌کند که حاوی خاصیتی به نام code و مقدار "ENOENT" می‌باشد. این مقدار کمی ناآشنا به نظر می‌رسد، کدهای خطا در Node از Unix الهام گرفته شده اند.

شیء statsای که توسط تابع stat برگردانده‌ می‌شود اطلاعاتی مانند اندازه فایل (خاصیت size) و تاریخ تغییر آن (mtime) را در اختیار ما می‌گذارد. در اینجا ما می‌خواهیم بدانیم که درخواست ما فایل معمولی بوده است یا یک پوشه، که متد isDirectory این موضوع را مشخص می‌کند.

از متد readdir برای خواندن مجموعه‌ای از فایل‌های یک پوشه و بازگرداندن آن به کلاینت استفاده می‌کنیم. برای فایل‌های عادی، یک استریم قابل خواندن به وسیله‌ی createReadStream ایجاد کرده و آن را به عنوان بدنه به همراه نوع محتوایی که بسته‌ی mime برای نام فایل داده شده مشخص می‌کند باز می‌گردانیم.

کدی که درخواست‌های DELETE را رسیدگی می‌کند کمی ساده‌تر است.

const {rmdir, unlink} = require("fs").promises;

methods.DELETE = async function(request) {
  let path = urlPath(request.url);
  let stats;
  try {
    stats = await stat(path);
  } catch (error) {
    if (error.code != "ENOENT") throw error;
    else return {status: 204};
  }
  if (stats.isDirectory()) await rmdir(path);
  else await unlink(path);
  return {status: 204};
};

هنگامی که یک پاسخ HTTP حاوی هیچ داده‌ای نیست، کد وضعیت 204 (“no content”) را می‌توان برای مشخص کردن آن استفاده کرد. با توجه به اینکه پاسخ به یک درخواست حذف (DELETE) نیازمند ارسال اطلاعاتی به جز اینکه عملیات موفق بوده یا خیر نیست، استفاده از این کد وضعیت در اینجا منطقی است.

ممکن است تعجب کنید که چرا تلاش برای حذف فایلی که موجود نیست وضعیت کد موفقیت برمی‌گرداند نه کد وضعیت خطا. زمانی که فایلی که باید حذف شود موجود نیست، می‌توانید پاسخ دهید که هدف درخواست پیش از این برآورده شده است. استاندارد HTTP پیشنهاد می‌کند که درخواست‌ها را تکرار شونده (idempotent) بسازیم، به این معنا که اگر درخواست مشابهی را چندین بار ارسال کنیم، پاسخی که دریافت می‌کنیم مشابه این باشد که آن درخواست یک بار ارسال شده است. به عبارتی، اگر سعی کنید که چیزی را که پیش از این حذف شده است حذف کنید، اثر مورد نظر برآورده شده است -آن فایل دیگر موجود نیست.

گرداننده برای درخواست‌های PUT در اینجا آمده است:

const {createWriteStream} = require("fs");

function pipeStream(from, to) {
  return new Promise((resolve, reject) => {
    from.on("error", reject);
    to.on("error", reject);
    to.on("finish", resolve);
    from.pipe(to);
  });
}

methods.PUT = async function(request) {
  let path = urlPath(request.url);
  await pipeStream(request, createWriteStream(path));
  return {status: 204};
};

این‌بار نیازی نیست بررسی کنیم که فایل مورد نظر وجود دارد یا خیر- اگر وجود داشت، کافی است تا باز نویسی‌اش کنیم. دوباره از ‍pipe برای انتقال داده‌ها از یک استریم قابل خوانده به قابل نوشتن استفاده می‌کنیم، در این مورد از یک درخواست به یک فایل. اما چون pipe طوری نوشته نشده است که یک promise برگرداند، باید برایش یک پوشش ایجاد کنیم، که همان pipeStream است و یک promise پیرامون خروجی pipe ایجاد می‌کند.

هنگامی‌ که در زمان باز نمودن فایل، مشکلی پیش‌ می‌آید، createWriteStream همچنان یک استریم برمی‌گرداند اما آن استریم یک رخداد "error" را تولید می‌کند. استریم خروجی درخواست نیز ممکن است متوقف شود، به عنوان مثال، اگر شبکه از دسترس خارج شود. بنابراین ما رخداد‌های ‍"error" هر دو استریم را در نظر می‌گیریم تا promise را لغو (reject) کنیم. وقتی کار pipe به اتمام می‌رسد استریم خروجی را می‌بندد که باعث می‌شود رخداد ‍‍"finish" تولید شود. این نقطه ای است که ما می‌توانیم با موفقیت promise را به سرانجام برسانیم (بدون بازگرداندن چیزی).

اسکریپت کامل این سرویس‌دهنده در آدرس https://eloquentjavascript.net/code/file_server.js موجود است. می توانید آن را دانلود کرده و پس از نصب وابستگی‌هایش، آن را به وسیله‌ی Node اجرا کنید و سرویس‌هنده‌ی فایل خودتان را داشته باشید. و البته که می توانید آن را تغییر دهید و با حل تمرین‌های این فصل آن را بهبود ببخشید.

ابزار خط فرمان curl، که به طور گسترده در سیستم‌های مبتنی بر Unix مانند مک و لینوکس در دسترس است، را می‌توان برای ایجاد درخواست‌های HTTP استفاده کرد. دستور پیش‌رو به صورت مختصر سرویس‌دهنده‌ی ما را آزمایش می‌کند. گزینه‌ی -X برای تنظیم متد درخواست استفاده می‌شود و -d برای قرار دادن بدنه‌ی درخواست.

$ curl http://localhost:8000/file.txt
File not found
$ curl -X PUT -d hello http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
hello
$ curl -X DELETE http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
File not found

اولین درخواست برای file.txt با شکست روبرو می‌شود زیرا فایل هنوز وجود ندارد. درخواست PUT فایل را ایجاد می کند و درخواست بعدی آن را با موفقیت برمی‌گرداند. پس از حذف آن به وسیله‌ی درخواست DELETE، فایل دوباره ناموجود می‌شود.

خلاصه

Node دوست داشتنی است، سیستمی کوچک که برای ما امکان اجرای جاوااسکریپت در فضایی غیر از مرورگر را فراهم می‌سازد. هدف ابتدایی طراحی Node برای استفاده در وظایف شبکه بود که قرار بود نقش یک گره (node) را در شبکه بازی کند. اما توانست خود را به همه‌ی وظایف و کاربردهای اسکریپتی عرضه کند، و اگر شما از نوشتن جاوااسکریپت لذت می‌برید، با استفاده از Node می‌توانید کارهایتان را خودکار نمایید.

NPM برای هر چیزی که فکرش را بکنید‌ (و چیزهایی که شاید فکرش را هم نکنید) بسته‌هایی فراهم می‌کند، و می‌توانید آن‌ها را بارگیری و به وسیله‌ی برنامه‌ی npm نصب کنید. خود Node نیز تعدادی ماژول درونی دارد،‌ مانند ماژول fs برای کار با سیستم فایل و http برای اجرای سرویس‌دهنده‌های HTTP و ساختن درخواست‌های HTTP.

همه‌ی عملیات ورودی و خروجی در Node به صورت ناهمگام صورت می‌پذیرند مگر اینکه صراحتا از شکل همگام یک تابع مانند readFileSync استفاده کنید. زمانی که از این گونه توابع ناهمگام استفاده می‌کنید، شما یک تابع callback به تابع مورد نظر ارسال می‌کنید و Node آن‌ها با یک مقدار خطا و (درصورت وجود) یک نتیجه فراخوانی می کند.

تمرین‌ها

ابزار جستجو

در سیستم‌های مبتنی بر Unix، ابزار خط فرمانی به نام grep وجود دارد که می‌توان از آن برای جستجوی فایل‌ها بر اساس یک عبارت باقاعده استفاده کرد.

یک اسکریپت Node بنویسید که بتوان آن را از طریق خط فرمان اجرا نمود و کاری مشابه grep را انجام دهد. اولین دستور ورودی از خط فرمان را به عنوان عبارت باقاعده در نظر بگیرد و دیگر ورودی‌ها را به عنوان فایل‌هایی که باید جستجو کند. خروجی این ابزار باید نام‌ فایل‌هایی باشد که محتوایشان با عبارت باقاعده مطابقت داشته باشد.

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

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

ورودی اول ابزار خط فرمان شما، همان عبارت باقاعده، را می توان در ‍process.argv[2]‍‍ به دست آورد. فایل‌های ورودی بعد از آن می‌آیند. می توانید از سازنده‌ی RegExp برای ساخت عبارت باقاعده از یک رشته استفاده کنید.

انجام این کار به صورت همگام، با استفاده از readFileSync، سرراست‌تر است، اما اگر دوباره از fs.promises برای گرفتن توابعی که promise برمی‌گردانند استفاده کنید و یک تابع async بنویسید، کد شما یکسان خواهد بود.

برای فهمیدن اینکه چیزی از جنس پوشه است، می توانید دوباره از stat یا statSync و متد isDirectory شیء stats استفاده کنید.

کاوش یک پوشه یک پردازش درختی است. می توانید این کار را یا با استفاده از یک تابع بازگشتی یا با نگه‌داری آرایه‌ای از کارها (فایل‌هایی که همچنان نیاز است کاوش شوند) انجام دهید. برای یافتن فایل‌های یک پوشه، می توانید readdir یا readdirSync را فراخوانی کنید. روش عجیب استفاده از حروف بزرگ در نام‌گذاری توابع در سیستم فایل Node از توابع استاندارد Unix الهام گرفته مانند readdir که تماما با حروف کوچک است، اما Sync را با حروف بزرگ به آن اضافه می‌کند.

برای بدست آوردن یک نام مسیر کامل از نام فایلی که با readdir به دست آمده است، باید آن را با نام پوشه ترکیب کنید و یک کاراکتر اسلش (/) بین‌شان قرار دهید.

ایجاد پوشه

اگر در سرویس‌دهنده‌ی فایل ما، متد DELETE قادر است تا پوشه‌ها را حذف کند (به وسیله‌ی rmdir)، هنوز امکان ایجاد یک پوشه را پشتیبانی نمی‌کند.

پشتیبانی از متد MKCOL (“make collection”) را اضافه کنید، که باید پوشه‌ای را به وسیله‌ی فراخوانی mkdir از ماژول fs ایجاد کند. MKCOL متدی نیست که زیاد استفاده شود در HTTP اما به هر حال برای این هدف در استاندارد WebDAV وجود دارد، که مجموعه‌ای قرارداد در HTTP مشخص می‌کند که آن را مناسب ایجاد کردن اسناد می‌سازد.

می‌توانید از تابعی که متد DELETE را پیاده‌سازی می‌کند به عنوان نقطه‌ی شروع برای متد MKCOL استفاده کنید. زمانی که فایلی پیدا نمی‌شود، سعی کنید تا پوشه‌ای را به وسیله‌ی mkdir ایجاد کنید. زمانی که پوشه‌ی مورد نظر در مسیر داده شده موجود بود، می توانید یک پاسخ 204 ارسال کنید که نشان دهید درخواست‌های ایجاد پوشه تکرارشونده یا idempotent می‌باشند. اگر فایلی غیرپوشه در اینجا وجود داشت، یک کد خطا برگردانید. کد 400 (“bad request”) مناسب خواهد بود.

یک فضای عمومی در وب

با توجه به اینکه سرویس‌دهنده‌ی فایل ما هر نوعی از فایل را پشتیبانی می‌کند و حتی سرنام صحیح برای Content-Type در نظر می‌گیرد، می توانید از آن برای میزبانی یک وب‌سایت استفاده کنید. به دلیل اینکه این سرویس‌دهنده اجازه حذف و جایگزینی فایل‌ها را به همه می‌دهد، در نوع خودش وب‌سایت جالبی خواهد شد: وب‌سایتی که می توان‌ آن را توسط هرکسی که زمان داشته باشد تا درخواست‌های HTTP صحیح ارسال کند، تغییر داد، بهبود بخشید و خراب کرد .

یک صفحه‌ی HTML ساده به همراه یک فایل جاوااسکریپت ساده ایجاد کنید. فایل‌ها را درون یک پوشه قرار داده و توسط سرویس‌دهنده‌ی پشتیبانی و در مرورگرتان باز کنید.

سپس، به عنوان یک تمرین پیشرفته یا حتی یک پروژه برای آخر هفته، تمام دانشی که در این کتاب کسب کرده اید را به کار ببرید تا رابط کاربرپسند‌تری برای ایجاد تغییر در این وب‌سایت ایجاد کنید- از درون خود وب‌سایت.

از یک فرم HTML برای ویرایش محتوای فایل‌های سازنده‌ی وب‌سایت استفاده کنید و به کاربر این امکان را بدهید که توسط درخواست‌های HTTP آن ها را به‌روزرسانی کند، همانطور که در فصل 18 توضیح داده شده است.

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

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

برای نگه‌داری محتوای فایلی که در حال ویرایش است می‌توانید از یک <textarea> استفاده کنید. یک درخواست‌ ‍GET به وسیله‌ی fetch می‌تواند برای دریافت محتوای یک فایل استفاده شود. می‌توانید به جای استفاده از http://localhost:8000/index.html، از URL های نسبی مثل index.html استفاده کنید تا به فایل‌هایی که روی سرویس‌دهنده‌ی یکسانی با اسکریپت اجرایی هستند ارجاع دهید.

سپس، هنگامی که کاربر روی یک دکمه (می توانید از یک عنصر <form> و رخداد "submit" استفاده کنید) کلیک می کند، یک درخواست PUT به URL مشابه ارسال کنید که در این درخواست محتوای <textarea> به عنوان بدنه‌ی درخواست ارسال می‌شود تا فایل مورد نظر ذخیره شود.

در ادامه می‌توانید یک عنصر <select> که حاوی همه‌ی فایل‌ها موجود در پوشه‌ی بالایی سرویس‌دهنده است اضافه کنید. گزینه‌ها را به وسیله‌ی خطوطی که به وسیله‌ی ارسال درخواست GET به آدرس / دریافت می‌شوند توسط <option> تنظیم کنید. زمانی که کاربر فایل دیگری را انتخاب می‌کند (یک رخداد change روی فیلد)، اسکریپت باید آن فایل را دریافت و نمایش دهد. در هنگام ذخیره‌ی یک فایل، از نام فایل انتخاب شده‌ی فعلی استفاده کنید.