منابع آموزشی و پژوهشی، برای دانشگاه و مدرسه
پشتیبانیانتشار محتوا
دورهٔ آموزشیرایگان

آموزش ساخت HTTP Server با Node.js؛ صفحه، API و TLS

۸ درس در ۴ فصل۹ ساعت و ۴۰ دقیقهدربارهٔ دوره
ساخت صفحهٔ سلام، سرور!درس ۲ از ۸

ساخت صفحهٔ مرتب و تعاملی؛ فرم، پاسخ API و وضعیت سرور

۷۰ دقیقه آموزش متنی
در این درس ۶ بخش

در این درس ظاهر واقعی «سلام، سرور!» را می‌سازیم: عنوان خوانا، فرم کوتاه، جعبهٔ پاسخ و کارت وضعیت. صفحه فقط دو کار دارد تا کاربر بداند از کجا شروع کند و نتیجه را کجا ببیند. سه فایل جدا می‌نویسیم و مسئولیت هرکدام را می‌شناسیم. کدها کامل‌اند، اما پاسخ تعاملی پس از اضافه‌کردن فایل‌های سرور در درس سوم فعال می‌شود. اگر فعلاً فایل‌ها را می‌نویسید، نبود API به معنی خراب‌بودن CSS نیست. نام و پوشه را دقیق نگه دارید تا هنگام اجرا دنبال خطای مسیر نگردیم.

HTML ساختار را می‌سازد؛ جای نتیجه را روشن کنید

HTML زبان نشانه‌گذاری است: با عنوان، بند، فرم و دکمه به محتوا ساختار می‌دهد. main محتوای اصلی و form محل ورود نام را مشخص می‌کنند. lang="fa" زبان متن و dir="rtl" جهت پایه را معرفی می‌کنند؛ راست‌چین‌کردن ظاهر جای تعیین جهت سند نیست. charset برابر UTF-8 است تا حروف فارسی با قرارداد روشنی خوانده شوند. متای viewport نیز به مرورگر موبایل کمک می‌کند عرض دستگاه را برای چیدمان در نظر بگیرد. این تنظیم‌ها را داخل head نگه دارید و ساختار عنوان‌ها را برای شبیه‌کردن اندازه‌ها به یکدیگر به‌هم نزنید؛ ظاهر را در CSS تغییر می‌دهیم.

داخل hello-http پوشهٔ public را بسازید؛ از Explorer ویرایشگر و New Folder می‌توانید استفاده کنید. فایل public/index.html را بسازید و همهٔ کد زیر را در آن ذخیره کنید. این پوشه برای فایل‌های قابل ارسال به مرورگر است؛ کلید خصوصی یا تنظیم محرمانه را کنار صفحه نگذارید. در این پروژه، سرور فقط مسیرهای ازپیش‌مشخص را می‌فرستد. پسوند واقعی باید html باشد. پس از نوشتن هر فایل Ctrl+S بزنید؛ خروجی از فایل روی دیسک می‌آید، نه از تغییری که هنوز فقط در تب ویرایشگر دیده می‌شود. فعلاً تمام نام‌ها و شناسه‌های نسخهٔ اصلی را حفظ کنید.

html
<!doctype html>
<html lang="fa" dir="rtl">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="یک پروژهٔ کوچک برای دیدن ارتباط صفحهٔ وب با HTTP Server و API در Node.js.">
  <title>سلام، سرور! | پروژهٔ درس‌آوند</title>
  <link rel="stylesheet" href="/style.css">
  <script src="/app.js" defer></script>
</head>
<body>
  <div class="page">
    <header class="topbar">
      <a class="brand" href="/" aria-label="سلام، سرور! صفحهٔ اصلی">
        <span class="brand-icon" aria-hidden="true">↗</span><span>درس‌آوند<span class="brand-sub">ساختن، بهترین راه یادگرفتن است.</span></span>
      </a>
      <span class="project-tag"><span aria-hidden="true">●</span> پروژهٔ اول من</span>
    </header>
    <main>
      <section class="intro" aria-labelledby="page-title">
        <span class="eyebrow">از یک صفحه، تا اولین پاسخ واقعی</span>
        <h1 id="page-title">سلام، <span>سرور!</span></h1>
        <p class="lead">یک نام بنویسید و پاسخ را از سرور بگیرید.<br>همین ارتباط کوچک، نقطهٔ شروع ساختن یک وب‌اپلیکیشن است.</p>
        <div class="route-line" aria-label="مسیر درخواست"><span>مرورگر شما</span><span aria-hidden="true">←</span><code>HTTP + API</code><span aria-hidden="true">←</span><span>سرور Node.js</span></div>
      </section>
      <div class="workspace">
        <section class="greet-card" aria-labelledby="form-title">
          <div class="card-top"><span class="step-number" aria-hidden="true">۰۱</span><span class="small-label">یک درخواست بفرستید</span><code class="route-tag">POST /api/greet</code></div>
          <h2 id="form-title">بیایید آشنا شویم.</h2>
          <p class="card-description">نام آزمایشی وارد کنید؛ چیزی در این پروژه ذخیره نمی‌شود.</p>
          <form id="greet-form" action="/api/greet" method="post">
            <label for="name">دوست دارید با چه نامی سلام کنیم؟</label>
            <div class="input-row"><input id="name" name="name" type="text" maxlength="60" required autocomplete="off" placeholder="مثلاً: سارا" aria-describedby="name-help"><button id="send-button" type="submit">دریافت سلام <span aria-hidden="true">←</span></button></div>
            <p id="name-help" class="field-help">برای تمرین، یک نام کوتاه و ساختگی کافی است.</p>
          </form>
          <div id="reply" class="reply-box" role="status" aria-live="polite" aria-atomic="true"><span class="reply-label">پاسخ سرور</span><p id="reply-text">منتظر اولین سلام شما هستیم…</p></div>
          <noscript><p class="noscript">برای ارسال فرم تعاملی، JavaScript مرورگر باید فعال باشد.</p></noscript>
        </section>
        <aside class="status-card" aria-labelledby="status-title">
          <div class="card-top"><span class="step-number" aria-hidden="true">۰۲</span><span class="small-label">ارتباط را بررسی کنید</span></div>
          <h2 id="status-title">آن سوی درخواست</h2>
          <p class="card-description">این دکمه وضعیت همین برنامه را از API می‌پرسد.</p>
          <div class="server-visual" aria-hidden="true"><div class="server-unit"><i></i><b></b></div><div class="server-unit"><i></i><b></b></div><div class="server-unit"><i></i><b></b></div><span class="server-base"></span></div>
          <div id="server-status" class="server-status" role="status" aria-live="polite" aria-atomic="true"><span id="status-dot" class="status-dot" aria-hidden="true"></span><span id="status-text">هنوز بررسی نشده</span></div>
          <p id="uptime" class="uptime">با یک کلیک، پاسخ واقعی را ببینید.</p>
          <button id="status-button" class="secondary-button" type="button">بررسی وضعیت سرور <span aria-hidden="true">↻</span></button>
          <code class="status-route">GET /api/status</code>
        </aside>
      </div>
      <section class="explanation" aria-label="در این پروژه چه اتفاقی می‌افتد؟"><span class="explanation-icon" aria-hidden="true">i</span><p><strong>پشت این صفحه چه می‌گذرد؟</strong> مرورگر با <code>fetch</code> درخواست می‌فرستد؛ برنامهٔ Node.js آن را بررسی می‌کند و یک پاسخ <code>JSON</code> برمی‌گرداند. متن پاسخ با ایمنی در همین صفحه نمایش داده می‌شود.</p></section>
    </main>
    <footer><span>یک پروژهٔ ساده، یک قدم واقعی.</span><span class="footer-tech" dir="ltr">HTML · CSS · JavaScript · Node.js</span></footer>
  </div>
</body>
</html>

label با for به input متصل است؛ نام فیلد فقط نوشتهٔ محوشوندهٔ داخل کادر نیست. دکمه submit است تا Enter هم فرم را بفرستد. required و maxlength به کاربر راهنما می‌دهند، ولی کنترل سرور لازم می‌ماند؛ درخواست می‌تواند بیرون از این فرم ساخته شود. حد ۶۰ با طول رشتهٔ جاوااسکریپت سنجیده می‌شود و بعضی نشانه‌ها مانند ایموجی بیش از یک واحد طول می‌گیرند؛ برای تمرین، نامی کوتاه و ساختگی کافی است. مدخل #forms-input-validation تفاوت راهنمای مرورگر و بررسی دادهٔ رسیده به برنامه را توضیح می‌دهد. خطای فرم را با پاک‌کردن ورودی کاربر پنهان نکنید.

ناحیهٔ پاسخ و وضعیت با aria-live="polite" معرفی شده‌اند تا ابزار خواندن صفحه بتواند تغییر نتیجه را اعلام کند. تزئینات برای خوانده‌شدن اجباری معرفی نشده‌اند. خطا فقط با رنگ قرمز انتقال پیدا نمی‌کند؛ متن هم مشکل را می‌گوید. این انتخاب‌ها به استفادهٔ واقعی کمک می‌کنند: کاربر صفحه‌کلید یا صفحه‌خوان باید نام را وارد کند، ارسال را پیدا کند و نتیجه را بفهمد. بعد از کامل‌شدن برنامه، با Tab حرکت و با Enter ارسال کنید. اگر تمرکز دکمه دیده نشود یا نوشته فقط در رنگ پنهان باشد، ظاهر زیبا هنوز تجربهٔ قابل استفاده‌ای نساخته است.

CSS، فاصله و چیدمان پاسخ‌گو را بسازید

فایل public/style.css را بسازید و کد کامل زیر را ذخیره کنید. CSS با انتخابگر مشخص می‌کند کدام عناصر چه ظاهری داشته باشند؛ class برای گروهی از عناصر و id برای یک عنصر مشخص به کار می‌رود. رنگ، فاصله و چیدمان در این فایل است و دادهٔ پاسخ جای دیگری مدیریت می‌شود. روی صفحهٔ پهن دو کارت کنار هم قرار می‌گیرند؛ روی صفحهٔ باریک زیر هم می‌آیند. این یعنی طراحی پاسخ‌گو: محتوای واحد با فضای دستگاه سازگار می‌شود. برای شروع، همین ساختار را نگه دارید و فقط یک رنگ یا فاصله را تغییر دهید تا اثر آن قابل تشخیص باشد.

css
:root {
  color-scheme:light;
  --paper:#f8f5ed;
  --ink:#163f33;
  --muted:#596b5f;
  --line:#d9e0d4;
  --green:#1d5b45;
  --soft:#edf1e7;
  --ochre:#936323;
  --white:#fffef9;
  --red:#a13b32
}
* {
  box-sizing:border-box
}
body {
  margin:0;
  background:var(--paper);
  color:var(--ink);
  font-family:Tahoma,"Segoe UI",Arial,sans-serif;
  font-size:15px;
  line-height:1.9
}
button,input {
  font:inherit
}
button,a,input {
  -webkit-tap-highlight-color:transparent
}
a {
  color:inherit;
  text-decoration:none
}
button {
  cursor:pointer
}
button:disabled {
  cursor:wait;
  opacity:.65
}
button:focus-visible,a:focus-visible {
  outline:3px solid var(--ochre);
  outline-offset:5px
}
input:focus-visible {
  outline:3px solid #b4cbb9;
  outline-offset:2px
}
code {
  font-family:Consolas,"Courier New",monospace;
  direction:ltr;
  unicode-bidi:embed
}
.page {
  max-width:1170px;
  margin:auto;
  padding:0 38px
}
.topbar {
  display:flex;
  align-items:center;
  justify-content:space-between;
  padding:27px 0 25px;
  border-bottom:1px solid var(--line)
}
.brand {
  display:flex;
  align-items:center;
  gap:12px;
  font-weight:700;
  font-size:23px;
  line-height:1.5
}
.brand-icon {
  display:grid;
  place-items:center;
  width:42px;
  height:46px;
  background:var(--green);
  color:#fff;
  border-radius:14px 4px 14px 14px;
  font-size:30px;
  font-weight:400
}
.brand-sub {
  display:block;
  color:var(--muted);
  font-size:12px;
  font-weight:400;
  letter-spacing:0;
  margin-top:1px
}
.project-tag {
  font-size:13px;
  color:var(--muted);
  background:var(--soft);
  border:1px solid var(--line);
  border-radius:30px;
  padding:4px 12px
}
.project-tag>span {
  font-size:10px;
  color:var(--ochre);
  margin-left:5px
}
.intro {
  text-align:center;
  padding:44px 0 31px
}
.eyebrow {
  font-size:13px;
  color:var(--ochre);
  letter-spacing:.1px
}
h1 {
  font-size:clamp(42px,5vw,64px);
  letter-spacing:-2.4px;
  line-height:1.3;
  margin:14px 0 16px;
  font-weight:800
}
h1>span {
  color:var(--green)
}
.lead {
  color:var(--muted);
  font-size:16px;
  line-height:2.1;
  margin:0
}
.route-line {
  display:inline-flex;
  align-items:center;
  justify-content:center;
  gap:14px;
  font-size:12px;
  background:var(--soft);
  border-radius:20px;
  padding:6px 19px;
  margin-top:20px;
  color:var(--muted)
}
.route-line>span[aria-hidden] {
  color:#96a995
}
.route-line code {
  font-size:13px;
  color:var(--green)
}
.workspace {
  display:grid;
  grid-template-columns:minmax(0,1.7fr) minmax(0,1fr);
  gap:22px;
  align-items:stretch
}
.greet-card,.status-card {
  background:var(--white);
  border:1px solid var(--line);
  border-radius:24px;
  padding:28px 29px 26px
}
.card-top {
  display:flex;
  align-items:center;
  gap:9px;
  min-height:27px
}
.step-number {
  display:grid;
  place-items:center;
  font-size:13px;
  width:28px;
  height:28px;
  border:1px solid #d9e2d5;
  border-radius:50%;
  color:var(--green)
}
.small-label {
  font-size:12px;
  color:var(--muted)
}
.route-tag {
  margin-right:auto;
  font-size:11px;
  color:var(--muted);
  background:var(--soft);
  padding:3px 8px;
  border-radius:7px
}
h2 {
  font-size:24px;
  letter-spacing:-.8px;
  margin:23px 0 7px;
  line-height:1.5
}
.card-description {
  font-size:13px;
  color:var(--muted);
  margin:0 0 25px
}
.greet-card form {
  margin-top:32px
}
label {
  display:block;
  font-size:14px;
  font-weight:700;
  margin-bottom:10px
}
.input-row {
  display:flex;
  gap:10px
}
input {
  background:#fff;
  border:1px solid #d2dbce;
  border-radius:10px;
  min-width:0;
  flex:1;
  padding:11px 13px;
  color:var(--ink);
  width:100%;
  font-size:15px
}
input::placeholder {
  color:#596b5f;
  font-size:14px
}
.input-row button {
  border:0;
  border-radius:10px;
  background:var(--green);
  color:#fff;
  padding:10px 17px;
  font-size:13px;
  white-space:nowrap;
  display:flex;
  align-items:center;
  justify-content:center;
  gap:18px;
  min-height:46px
}
.input-row button:hover:not(:disabled) {
  background:#144b37
}
.field-help {
  font-size:11px;
  color:var(--muted);
  margin:9px 1px 0
}
.reply-box {
  background:#f0f4ea;
  border:1px solid #dde7d6;
  border-radius:13px;
  margin-top:27px;
  padding:16px 18px;
  min-height:100px;
  overflow-wrap:anywhere
}
.reply-label {
  font-size:11px;
  color:var(--muted);
  display:flex;
  align-items:center;
  gap:6px
}
.reply-label::before {
  content:"";
  height:5px;
  width:5px;
  border-radius:50%;
  background:#7b9c72
}
.reply-box p {
  font-size:15px;
  color:var(--green);
  margin:9px 0 0;
  line-height:1.9
}
.reply-box.is-error {
  background:#fcf0ea;
  border-color:#ecd6cc
}
.reply-box.is-error p {
  color:var(--red)
}
.reply-box.is-error .reply-label::before {
  background:var(--red)
}
.status-card {
  background:#eef2e7;
  display:flex;
  flex-direction:column;
  align-items:stretch
}
.status-card h2 {
  font-size:22px;
  margin-top:23px
}
.status-card .card-description {
  margin-bottom:15px
}
.server-visual {
  width:116px;
  margin:4px auto 12px;
  position:relative;
  padding-bottom:12px;
  transform:rotate(-5deg)
}
.server-unit {
  height:24px;
  border-radius:5px;
  margin-top:4px;
  background:linear-gradient(135deg,#305e48,#143a2b);
  box-shadow:3px 4px 0 #c8d5be;
  display:flex;
  align-items:center;
  padding:0 10px;
  justify-content:space-between
}
.server-unit i {
  height:5px;
  width:5px;
  border-radius:50%;
  background:#debd69;
  box-shadow:0 0 6px #d3b361
}
.server-unit b {
  height:3px;
  width:24px;
  background:#64836b;
  border-radius:2px
}
.server-base {
  position:absolute;
  left:-9px;
  right:-9px;
  bottom:0;
  height:5px;
  background:#d6dfcb;
  border-radius:50%
}
.server-status {
  display:flex;
  align-items:center;
  justify-content:center;
  gap:7px;
  font-size:14px;
  font-weight:700;
  margin:3px 0
}
.status-dot {
  width:7px;
  height:7px;
  background:#a6b197;
  border-radius:50%
}
.status-dot.is-online {
  background:#3a8a4b;
  box-shadow:0 0 0 4px #dce9d6
}
.status-dot.is-error {
  background:var(--red)
}
.uptime {
  text-align:center;
  font-size:12px;
  color:var(--muted);
  margin:5px 0 17px;
  min-height:20px
}
.secondary-button {
  display:flex;
  justify-content:space-between;
  align-items:center;
  width:100%;
  padding:9px 14px;
  background:transparent;
  border:1px solid #c7d2bd;
  border-radius:9px;
  color:var(--green);
  font-size:13px;
  margin-top:auto;
  min-height:42px
}
.secondary-button:hover:not(:disabled) {
  background:#e2eada
}
.secondary-button>span {
  font-size:19px;
  line-height:1
}
.status-route {
  display:block;
  font-size:11px;
  color:var(--muted);
  text-align:center;
  margin-top:10px
}
.explanation {
  display:flex;
  align-items:flex-start;
  gap:11px;
  padding:21px 8px 26px
}
.explanation-icon {
  font-family:Georgia,serif;
  font-weight:700;
  font-size:14px;
  border:1px solid #c4d0bc;
  min-width:21px;
  height:21px;
  border-radius:50%;
  display:grid;
  place-items:center;
  color:#7a8f70;
  margin-top:3px
}
.explanation p {
  font-size:12px;
  color:var(--muted);
  line-height:2.2;
  margin:0
}
.explanation strong {
  font-weight:700;
  color:#47664f
}
.explanation code {
  color:var(--green);
  font-size:13px
}
footer {
  display:flex;
  justify-content:space-between;
  gap:12px;
  border-top:1px solid var(--line);
  padding:18px 0 25px;
  font-size:12px;
  color:#596b5f
}
.footer-tech {
  font-family:Consolas,monospace;
  font-size:12px
}
.noscript {
  color:var(--red);
  font-size:14px
}
@media(min-width:1500px) {
  .intro {
    padding-top:55px
  }
}
@media(max-width:760px) {
  .page {
    padding:0 20px
  }
  .topbar {
    padding:21px 0
  }
  .brand {
    font-size:21px
  }
  .brand-sub {
    font-size:10px
  }
  .project-tag {
    font-size:11px;
    padding:4px 9px
  }
  .intro {
    padding:32px 0 25px
  }
  .lead {
    font-size:14px
  }
  .workspace {
    grid-template-columns:1fr;
    gap:17px
  }
  .greet-card,.status-card {
    padding:24px;
    border-radius:20px
  }
  .status-card .server-visual {
    margin-top:10px
  }
  .status-card .secondary-button {
    margin-top:0
  }
  .status-card .card-description {
    margin-bottom:5px
  }
  .explanation {
    padding-top:20px
  }
  .explanation p {
    font-size:12px
  }
  footer {
    flex-direction:column;
    align-items:center;
    text-align:center;
    gap:3px
  }
  .route-line {
    gap:9px;
    font-size:11px;
    padding:5px 12px
  }
  .route-line code {
    font-size:12px
  }
}
@media(max-width:400px) {
  .page {
    padding:0 15px
  }
  .input-row {
    flex-direction:column;
    gap:9px
  }
  .input-row button {
    min-height:43px
  }
  .route-tag {
    font-size:10px
  }
  .greet-card,.status-card {
    padding:20px
  }
  .card-top {
    gap:6px
  }
  .small-label {
    font-size:11px
  }
  h1 {
    letter-spacing:-1.6px
  }
  .brand-sub {
    display:none
  }
}
@media(prefers-reduced-motion:reduce) {
  * {
    scroll-behavior:auto
  }
}

فونت از خانواده‌های موجود روی دستگاه انتخاب می‌شود و به CDN وابسته نیستیم؛ شکل حروف ممکن است کمی متفاوت باشد. اگر صفحه بی‌رنگ دیده شد، بارگیری /style.css و نام فایل را بررسی کنید؛ styles.css با style.css یکی نیست. برنامه سه فایل صفحه را هنگام شروع در حافظه می‌خواند؛ پس بعد از ویرایش HTML یا CSS یا app.js، فایل را ذخیره، فرایند خودتان را با Ctrl+C متوقف و دوباره اجرا کنید، سپس مرورگر را تازه کنید. تازه‌سازی تنها، فایل را دوباره از دیسک نمی‌خواند. برای نمایش موبایل، عرض پنجره را کم کنید و بیرون‌نزدن ورودی و دکمه را ببینید.

جاوااسکریپت مرورگر را به API وصل کنید

فایل public/app.js را بسازید. در HTML با script دارای defer معرفی شده است؛ در اجرای معمول این نوع فایل، کد پس از تجزیهٔ سند اجرا می‌شود تا عناصر لازم موجود باشند. DOM مدل درختی صفحه است. querySelector کادر نام یا محل پاسخ را با شناسه پیدا می‌کند و متغیر، ارجاع همان عنصر را نگه می‌دارد. document به مرورگر تعلق دارد و این کد را نباید به‌جای app.mjs روی Node اجرا کرد. برای مرور انتخاب عنصر به #dom رجوع کنید. اگر شناسه‌ها را در HTML تغییر می‌دهید، انتخابگرهای جاوااسکریپت نیز باید مطابق شوند؛ در اولین اجرا، تغییرشان ندهید.

javascript
const form = document.querySelector('#greet-form');
const nameInput = document.querySelector('#name');
const sendButton = document.querySelector('#send-button');
const reply = document.querySelector('#reply');
const replyText = document.querySelector('#reply-text');
const statusButton = document.querySelector('#status-button');
const statusText = document.querySelector('#status-text');
const statusDot = document.querySelector('#status-dot');
const uptime = document.querySelector('#uptime');

const messages = {
  INVALID_NAME: 'یک نام کوتاه، حداکثر ۶۰ نویسه، وارد کنید؛ فقط فاصله کافی نیست.',
  INVALID_FIELDS: 'درخواست باید فقط یک نام متنی داشته باشد.',
  BODY_TOO_LARGE: 'درخواست بیش از اندازه بزرگ است؛ نام کوتاه‌تری وارد کنید.',
  INVALID_JSON: 'قالب درخواست درست نیست. صفحه را تازه کنید و دوباره تلاش کنید.'
};

async function requestJson(path, options = {}) {
  let response;
  try {
    response = await fetch(path, { ...options, signal: AbortSignal.timeout(8000) });
  } catch (error) {
    if (error.name === 'TimeoutError') throw new Error('پاسخ دیر رسید. اتصال را بررسی و دوباره تلاش کنید.');
    throw new Error('ارتباط با سرور برقرار نشد. روشن‌بودن برنامه و اتصال را بررسی کنید.');
  }
  let data;
  try { data = await response.json(); }
  catch { throw new Error('سرور پاسخ JSON قابل‌خواندن برنگرداند.'); }
  if (!response.ok) throw new Error(messages[data?.error] ?? `درخواست انجام نشد؛ کد پاسخ: ${response.status}`);
  return data;
}

form.addEventListener('submit', async event => {
  event.preventDefault();
  const name = nameInput.value.trim();
  if (!name || name.length > 60) {
    reply.classList.add('is-error');
    replyText.textContent = messages.INVALID_NAME;
    nameInput.focus();
    return;
  }
  sendButton.disabled = true;
  reply.classList.remove('is-error');
  replyText.textContent = 'درخواست شما در راه سرور است…';
  try {
    const data = await requestJson('/api/greet', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name })
    });
    if (typeof data?.message !== 'string') throw new Error('ساختار پاسخ سرور مطابق انتظار نیست.');
    replyText.textContent = data.message;
  } catch (error) {
    reply.classList.add('is-error');
    replyText.textContent = error.message;
  } finally { sendButton.disabled = false; }
});

statusButton.addEventListener('click', async () => {
  statusButton.disabled = true;
  statusText.textContent = 'در حال بررسی…';
  statusDot.className = 'status-dot';
  try {
    const data = await requestJson('/api/status');
    if (data?.ok !== true || data.service !== 'darsavand-hello' ||
        !Number.isSafeInteger(data.uptimeSeconds) || data.uptimeSeconds < 0) {
      throw new Error('ساختار پاسخ وضعیت مطابق انتظار نیست.');
    }
    statusDot.classList.add('is-online');
    statusText.textContent = 'سرور پاسخ می‌دهد';
    uptime.textContent = `${new Intl.NumberFormat('fa-IR').format(data.uptimeSeconds)} ثانیه از شروع برنامه`;
  } catch (error) {
    statusDot.classList.add('is-error');
    statusText.textContent = 'بررسی کامل نشد';
    uptime.textContent = error.message;
  } finally { statusButton.disabled = false; }
});

تابع requestJson بخش مشترک ارتباط است تا فرم و دکمهٔ وضعیت رفتارهای ناسازگار پیدا نکنند. fetch درخواست شبکه می‌فرستد و Promise برمی‌گرداند؛ #async-await دریافت نتیجه را خواناتر می‌کند. /api/greet یک نشانی نسبی به ریشهٔ همان مبدأ است. مبدأ ترکیب روش ارتباط، میزبان و پورت است؛ صفحه و API را از همان برنامه می‌گیریم تا برای این پروژه نیازی به فعال‌کردن CORS نباشد. بازکردن فایل با file:// یا یک سرور دیگر، این فرض را تغییر می‌دهد؛ در آن حالت مسیر نسبی لزوماً به Node مورد نظر نمی‌رسد. نشانی درست را از پیام اجرای برنامه بردارید.

رویداد submit پیش از ارسال پیش‌فرض گرفته می‌شود. preventDefault اجازه می‌دهد نام را در قالب #json بفرستیم: شیئی با ویژگی name به متن تبدیل می‌شود و Content-Type نوع بدنه را اعلام می‌کند. فرم HTML بدون این کد معمولاً چنین JSONی نمی‌سازد؛ به همین دلیل noscript فعال‌بودن جاوااسکریپت را یادآوری می‌کند. نام پس از trim باید خالی نباشد. با وجود کنترل مرورگر، سرور هم همان قرارداد ضروری را مستقل بررسی می‌کند. این برنامه نام را ذخیره نمی‌کند و اجازهٔ ورود نمی‌دهد؛ جملهٔ سلام فقط نتیجهٔ یک درخواست بدون ماندگاری است، نه ایجاد حساب یا ثبت‌نام کاربر.

response.ok هم بررسی می‌شود، چون رسیدن پاسخ شبکه به معنی موفقیت کار نیست. سرور می‌تواند ۴۰۴ یا ۴۲۲ را با ارتباط سالم برگرداند. سپس بدنه باید JSON قابل خواندن باشد و برای سلام، message متنی داشته باشد. در مسیر وضعیت نیز نام سرویس، ok و عدد زمان بررسی می‌شود. #fetch-http-json فرق خطای HTTP و خطای شبکه را روشن می‌کند. این کنترل‌ها جای بررسی سمت سرور را نمی‌گیرند؛ به رابط کمک می‌کنند پاسخ نامنتظر را به‌جای نتیجهٔ درست نمایش ندهد. جزئیات فنی را می‌توان در Network دید و برای کاربر پیام قابل فهم نشان داد.

درخواست مهلت دارد و دکمه تا پایان انتظار غیرفعال می‌شود؛ پیام میان‌راهی نشان می‌دهد کلیک ثبت شده است. finally دکمه را دوباره آزاد می‌کند تا شکست آن را برای همیشه قفل نکند. مقدار واردشده بعد از خطا پاک نمی‌شود و در خطای نام، تمرکز به کادر برمی‌گردد. پاسخ با textContent درج می‌شود؛ نوشتهٔ شبیه HTML باید متن دیده شود، نه به عنصر اجرایی تبدیل شود. بعداً نام ساختگی <b>سارا</b> را آزمایش کنید و انتظار متن ساده داشته باشید. این رفتار نمایش متن را بهتر می‌کند، ولی به‌تنهایی همهٔ مسائل امنیتی یک محصول بزرگ را حل نمی‌کند.

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

سه فایل مرورگر اکنون کامل‌اند. بعد از اضافه‌کردن فایل‌های سرور در درس سوم، برنامه را از ریشهٔ hello-http اجرا و http://127.0.0.1:3000 را باز کنید؛ index.html را دوبارکلیک نکنید. نخست دکمهٔ وضعیت را بزنید؛ باید وضعیت و زمان گذشته از شروع همین فرایند نمایش داده شود. این عدد، ساعت دستگاه یا مدت روشن‌بودن کل اوبونتو نیست. سپس «سارا» را وارد و فرم را ارسال کنید. پیام باید در جعبهٔ پاسخ بیاید و صفحهٔ کامل دوباره بارگیری نشود. اگر هر دو دکمه شکست خوردند، پیش از تغییر CSS، اجرای سرور و نشانی مرورگر را بررسی کنید.

در ابزار توسعهٔ مرورگر Network را باز کنید. فرم را بفرستید و درخواست /api/greet را انتخاب کنید؛ روش POST، نوع application/json و بدنهٔ نام را پیدا کنید. در Response متن ساختاریافتهٔ پیام را ببینید؛ پاسخ موفق این پروژه کد ۲۰۰ دارد. برای این مشاهده اطلاعات واقعی لازم نیست. وضعیت سبز فقط نشان می‌دهد برنامه در زمان همان بررسی پاسخ داده، نه اینکه همیشه سالم می‌ماند. اگر سرور متوقف شود، نتیجهٔ قبلی یک پایش دائمی نیست؛ دکمه را دوباره بزنید تا درخواست تازه برود. این تفاوت کوچک در طراحی صفحه‌های وضعیت واقعی هم مهم است.

برای مرور مبانی به این درس‌های مشخص برگردید:
انتخاب و ساخت عناصر DOM: https://www.darsavand.com/courses/javascript-from-zero-to-project?lesson=10c926a6-eac6-4724-95ba-9fb64e1bfc07
رویدادها و فرم‌ها: https://www.darsavand.com/courses/javascript-from-zero-to-project?lesson=a97cd3de-7c55-4276-9869-9126279718ad
اگر Console خطای عنصر پیدا‌نشده نشان داد، شناسه‌ها، نام app.js و defer را مقایسه کنید. اگر ظاهر درست است ولی JSON نمی‌رسد، ترمینال و Network را ببینید. افزودن فاصله و رنگ، برنامهٔ خاموش یا مسیر API اشتباه را اصلاح نمی‌کند. هر بار یک علت را بررسی کنید، همان درخواست را تکرار کنید و نتیجه را یادداشت کنید تا بدانید کدام تغییر مؤثر بوده است.

تمرین با پاسخ و معیار تحویل

متن راهنمای نام و رنگ اصلی را عوض کنید، اما شناسه‌ها را حفظ کنید. پاسخ مورد انتظار تغییر ظاهر و کارکرد قبلی فرم است؛ تغییر id بدون اصلاح انتخابگر، ارتباط جاوااسکریپت را می‌شکند. سپس نام معتبر، ورودی فقط فاصله و متن شبیه HTML را بیازمایید: اولی جواب می‌گیرد، دومی پس از trim رد می‌شود و سومی متن ساده می‌ماند. سرور خودتان را با Ctrl+C متوقف و وضعیت تازه بخواهید؛ باید پیام خطا ببینید و دکمه برای تلاش دوباره آزاد شود. تحویل درس، سه فایل ذخیره‌شده و گزارش این حالت‌هاست. ظاهر را در پنجرهٔ باریک و حرکت صفحه‌کلید هم بررسی کنید.

منابع رسمی برای مطالعهٔ بیشتر

MDN — HTML content: https://developer.mozilla.org/en-US/docs/Learn_web_development/Getting_started/Your_first_website/Creating_the_content
MDN — script and defer: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script
MDN — dir: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir
MDN — HTML accessibility: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Accessibility/HTML
MDN — Using Fetch: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
MDN — textContent: https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent
MDN — Media queries: https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Using
MDN — JSON.stringify: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
MDN — preventDefault: https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault

تألیف، سناریو و مثال‌های آموزشی: تحریریه درس‌آوند. منابع انگلیسی برای بررسی فنی و مطالعهٔ بیشتر معرفی شده‌اند.

ورود برای ثبت پیشرفت

متن درس‌های عمومی را بدون ورود بخوانید.

یادگیری‌ات را دنبال کن

برای نگهداری پیشرفت و ادامهٔ یادگیری از حساب خود استفاده کن.

ورود و ثبت‌نام
دربارهٔ این دوره

پروژهٔ رایگان «سلام، سرور!» در ۸ درس؛ ساخت یک صفحهٔ فارسی و API با Node.js، اجرای اوبونتو و HTTPS با Let’s Encrypt یا نصب دستی گواهی، همراه کد کامل و آزمون.

پرسشی دربارهٔ این درس داری؟گفت‌وگو با پشتیبانی در حساب کاربری
یکجا انتخاب کن، راحت‌تر دریافت کن

انتخاب‌های من ۰ منبع

مجموعهٔ مطالعه‌ات را بساز.

کنار مقاله‌ها «انتخاب» را بزنید تا چند فایل را با هم دریافت کنید.

انتخاب مقاله‌های رایگان