วิธีสร้างเว็บเซิร์ฟเวอร์ใน Node.js ด้วยโมดูล HTTP
บทนำ
เมื่อคุณดูเว็บเพจในเบราว์เซอร์ของคุณ คุณกำลังส่งคำขอไปยังคอมพิวเตอร์อีกเครื่องบนอินเทอร์เน็ต ซึ่งคอมพิวเตอร์นั้นจะให้เว็บเพจแก่คุณเป็นการตอบสนอง คอมพิวเตอร์ที่คุณติดต่อผ่านอินเทอร์เน็ตนี้เรียกว่า เว็บเซิร์ฟเวอร์ เว็บเซิร์ฟเวอร์จะรับคำขอ HTTP จากลูกค้า เช่น เบราว์เซอร์ของคุณ และให้การตอบสนอง HTTP เช่น หน้า HTML หรือ JSON จาก API
โมดูล http ของ Node.js มีทุกอย่างที่คุณต้องการเพื่อสร้างเว็บเซิร์ฟเวอร์โดยไม่ต้องพึ่งพาไลบรารีภายนอก มันจัดการคำขอที่เข้ามา แยกวิเคราะห์หัวข้อ HTTP จัดการการเชื่อมต่อ และส่งการตอบสนองทั้งหมดผ่าน API ของ JavaScript ที่เข้าใจง่าย
โมดูล http เป็นโซลูชันที่ติดตั้งมาในตัวของ Node.js สำหรับการสร้างเซิร์ฟเวอร์และไคลเอนต์ HTTP แตกต่างจากเฟรมเวิร์กอย่าง Express.js เพราะมันเป็นส่วนหนึ่งของแกนกลางของ Node.js หมายความว่าไม่จำเป็นต้องติดตั้งหรือมีการพึ่งพาเพิ่มเติม ทำให้เหมาะสำหรับแอปพลิเคชันที่เบา การเรียนรู้พื้นฐานของ HTTP หรือเมื่อต้องการควบคุมการจัดการคำขออย่างเต็มที่
การที่เซิร์ฟเวอร์จะแสดงหน้าเว็บจำเป็นต้องใช้ซอฟต์แวร์จำนวนมาก ซึ่งซอฟต์แวร์นี้โดยทั่วไปแบ่งออกเป็นสองประเภท: ส่วนหน้าและส่วนหลัง โค้ดส่วนหน้า เกี่ยวข้องกับวิธีการนำเสนอเนื้อหา เช่น สีของแถบนำทางและรูปแบบตัวอักษร โค้ดส่วนหลัง เกี่ยวข้องกับวิธีการแลกเปลี่ยน ประมวลผล และจัดเก็บข้อมูล โค้ดที่จัดการคำขอเครือข่ายจากเบราว์เซอร์ของคุณหรือสื่อสารกับฐานข้อมูลจะถูกจัดการหลักโดยโค้ดส่วนหลัง
Node.js ช่วยให้นักพัฒนาสามารถใช้ JavaScript เขียนโค้ดส่วนแบ็กเอนด์ได้ แม้ว่าตามปกติจะใช้ในเบราว์เซอร์เพื่อเขียนโค้ดส่วนหน้า การมีทั้งส่วนหน้าและส่วนหลังรวมกันแบบนี้ช่วยลดความพยายามในการสร้างเว็บเซิร์ฟเวอร์ ซึ่งนี่เป็นเหตุผลสำคัญที่ทำให้ Node.js เป็นตัวเลือกยอดนิยมสำหรับการเขียนโค้ดแบ็กเอนด์
บทเรียนนี้สาธิตวิธีการสร้างเว็บเซิร์ฟเวอร์ที่พร้อมใช้งานในสภาพแวดล้อมการผลิตโดยใช้โมดูล http ที่รวมอยู่ใน Node.js คุณจะได้สร้างเซิร์ฟเวอร์ที่จัดการการกำหนดเส้นทาง วิเคราะห์เนื้อหาของคำขอ ให้บริการไฟล์สถิตย์ ใช้งานการจัดการข้อผิดพลาดที่เหมาะสม และทำงานร่วมกับทั้ง CommonJS และ ES Module syntax
ทำให้การติดตั้งแอปพลิเคชัน Node.js ง่ายขึ้นด้วยแพลตฟอร์มแอป Linux-Console.net ส่งมอบ Node โดยตรงจาก GitHub ได้ภายในไม่กี่นาที
ประเด็นสำคัญ
ก่อนที่เราจะเริ่ม เข้าไปดูสิ่งที่คุณจะทำได้ดังนี้:
สร้างเซิร์ฟเวอร์ HTTP โดยใช้โมดูล
httpของ Node.js โดยไม่ใช้เฟรมเวิร์ก:
คุณจะได้เรียนรู้วิธีสร้างเซิร์ฟเวอร์เว็บ HTTP ที่ใช้งานได้เต็มรูปแบบ โดยใช้เพียงโมดูลhttpของ Node.js เท่านั้น ซึ่งหมายความว่าจะไม่ต้องพึ่งพาไลบรารีภายนอกหรือเฟรมเวิร์ก ทำให้คุณเข้าใจการทำงานของเซิร์ฟเวอร์ในระดับลึก รวมถึงสามารถสร้างการใช้งานที่เบาและยืดหยุ่นได้มากขึ้นตามความต้องการของคุณจัดการประเภทเนื้อหาต่าง ๆ (JSON, HTML, CSV, ไฟล์สแตติก) พร้อมหัวข้อที่เหมาะสม:
คุณจะได้เรียนรู้วิธีการทำงานกับหลายประเภทเนื้อหา ตอบกลับด้วย HTML สำหรับหน้าเว็บ, JSON สำหรับ API, ไฟล์ CSV หรือให้บริการไฟล์สแตติกเช่น ภาพและสคริปต์ คุณจะเข้าใจวิธีการตั้งค่า HTTP headers เช่นContent-Typeอย่างถูกต้อง เพื่อให้เบราว์เซอร์และลูกค้าแปลความหมายการตอบกลับของคุณอย่างถูกต้องเพื่อประสบการณ์ผู้ใช้ที่ราบรื่นดำเนินการกำหนดเส้นทางโดยใช้การแยกวิเคราะห์ URL ของคำขอและคำสั่ง switch:
การกำหนดเส้นทางเป็นพื้นฐานสำหรับเว็บเซิร์ฟเวอร์ ทำให้คุณสามารถให้บริการตรรกะที่แตกต่างกันสำหรับ URL และ endpoints ที่แตกต่างกัน คุณจะได้เรียนรู้เทคนิคในการแยกวิเคราะห์ URL ของคำขอที่เข้ามาและสร้างโครงสร้างตรรกะของเซิร์ฟเวอร์ของคุณ (ตัวอย่างเช่น ใช้คำสั่ง switch) เพื่อให้คำตอบที่กำหนดเองตามเส้นทางและวิธี HTTP จำลองพื้นฐานของสิ่งที่เฟรมเวิร์กขนาดใหญ่ทำเบื้องหลังประมวลผลคำขอ POST โดยการแยกวิเคราะห์เนื้อหาของคำขอและจัดการกับประเภทเนื้อหาต่าง ๆ:
การจัดการข้อมูลที่ผู้ใช้ส่งเข้ามาเป็นสิ่งสำคัญสำหรับแอปพลิเคชันเชิงโต้ตอบ คุณจะได้เรียนรู้วิธีจัดการคำขอ POST รวมถึงการอ่านและแยกวิเคราะห์เนื้อหาของคำขอในรูปแบบข้อมูลดิบ JSON หรือรูปแบบอื่น ๆ ส่วนนี้ครอบคลุมถึงการบัฟเฟอร์ข้อมูลที่รับเข้ามา การแยกวิเคราะห์อย่างปลอดภัย และการตอบสนองตามข้อมูลที่ได้รับ ซึ่งเป็นพื้นฐานสำคัญสำหรับการโต้ตอบกับผู้ใช้หรือการพัฒนา APIให้บริการไฟล์สแตติกอย่างมีประสิทธิภาพจากระบบไฟล์:
เซิร์ฟเวอร์เว็บหลายตัวจำเป็นต้องให้บริการไฟล์สแตติก—เช่น รูปภาพ สไตล์ชีท และไฟล์ JavaScript—โดยตรงกับผู้ใช้ คุณจะได้เรียนรู้วิธีการอ่านไฟล์จากดิสก์และส่งไฟล์เหล่านั้นอย่างมีประสิทธิภาพผ่าน HTTP การจัดการเส้นทางไฟล์อย่างรอบคอบเพื่อหลีกเลี่ยงปัญหาด้านความปลอดภัย และสำรวจการปรับปรุงประสิทธิภาพเช่นการสตรีมไฟล์เพื่อเพิ่มประสิทธิภาพสำหรับไฟล์ขนาดใหญ่ดำเนินการจัดการข้อผิดพลาดด้วยรหัสสถานะ HTTP ที่เหมาะสมและการบันทึก:
เซิร์ฟเวอร์ที่มั่นคงต้องสามารถจัดการข้อผิดพลาดได้อย่างราบรื่น คุณจะได้เรียนรู้วิธีตรวจจับ บันทึก และตอบสนองต่อข้อผิดพลาด เช่น คำขอไม่ถูกต้องหรือไฟล์ที่หายไป โดยใช้รหัสสถานะ HTTP มาตรฐานเช่น 400, 404, และ 500 นอกจากนี้คุณยังจะได้เรียนรู้แนวทางการบันทึกพื้นฐาน เพื่อให้ปัญหาสามารถติดตามได้และผู้ใช้ได้รับข้อความข้อผิดพลาดที่ชัดเจนโดยไม่เปิดเผยข้อมูลที่ละเอียดอ่อน-
ใช้ JavaScript สมัยใหม่ทั้งกับ CommonJS (
require) และ ES Modules (import):
Node.js รองรับสองระบบโมดูล: CommonJS และ ES Modules คุณจะได้ฝึกใช้ทั้งrequire()และimportพร้อมกับเข้าใจว่าเมื่อไหร่—และอย่างไร—ควรใช้แต่ละแบบ ซึ่งไม่เพียงแต่ช่วยให้โค้ดของคุณสามารถทำงานร่วมกับโปรเจกต์ Node.js ต่าง ๆ ได้ แต่ยังช่วยให้คุณเขียนโค้ด JavaScript ที่ทันสมัย สะอาด และดูแลรักษาได้ง่าย นำแนวทางปฏิบัติที่ดีที่สุดสำหรับการผลิตมาใช้ รวมถึงการเก็บแคชไฟล์ การกู้คืนข้อผิดพลาด และการพิจารณาด้านความปลอดภัย:
สุดท้าย คุณจะได้เรียนรู้แนวทางปฏิบัติที่สำคัญสำหรับการปรับใช้เซิร์ฟเวอร์เว็บ Node.js ในสภาพแวดล้อมจริง ซึ่งรวมถึงการใช้การเก็บแคชไฟล์เพื่อเพิ่มประสิทธิภาพ การนำกลยุทธ์การกู้คืนข้อผิดพลาดมาใช้เพื่อให้เซิร์ฟเวอร์ของคุณคงทน และการนำมาตรการรักษาความปลอดภัยพื้นฐาน (เช่น การทำความสะอาดข้อมูลนำเข้าและการตั้งค่า HTTP headers ที่เหมาะสม) มาใช้เพื่อปกป้องแอปพลิเคชันและผู้ใช้ของคุณ
ทำความเข้าใจโมดูล HTTP
โมดูล http เป็นอินเทอร์เฟซระดับต่ำของ Node.js สำหรับการสื่อสาร HTTP ซึ่งมีฟังก์ชันดังนี้:
- การสร้างเซิร์ฟเวอร์:
http.createServer()สร้างอินสแตนซ์เซิร์ฟเวอร์ HTTP - การจัดการคำขอ: คำขอที่เข้ามาจะแสดงโดยวัตถุ
IncomingMessage - การจัดการการตอบสนอง: การตอบสนองที่ส่งออกใช้วัตถุ
ServerResponse - รองรับการสตรีม: ทั้งการร้องขอและการตอบสนองเป็นสตรีม ทำให้การถ่ายโอนข้อมูลมีประสิทธิภาพ
ทำไมต้องใช้โมดูล HTTP แทน Express? แม้ว่า Express.js จะมีการย่อขั้นตอนที่สะดวกสบาย แต่โมดูล http แบบดั้งเดิมให้คุณควบคุมได้อย่างเต็มที่ ขนาดไฟล์บันเดิลที่เล็กลง และความเข้าใจเชิงลึกเกี่ยวกับพื้นฐานของ HTTP มันคือรากฐานที่เฟรมเวิร์กสร้างขึ้น สำหรับ API แบบง่ายๆ ไมโครเซอร์วิส หรือเพื่อการเรียนรู้ มักจะเป็นสิ่งที่คุณต้องการเท่านั้น
โมดูลนี้เป็นแบบขับเคลื่อนด้วยเหตุการณ์: เมื่อมีคำขอเข้ามา เซิร์ฟเวอร์จะส่งเหตุการณ์ 'request' และเรียกใช้ฟังก์ชันผู้ฟังคำขอของคุณ การออกแบบนี้ช่วยให้ Node.js สามารถจัดการการเชื่อมต่อจำนวนมากพร้อมกันได้อย่างมีประสิทธิภาพบนเธรดเดียวผ่านวงจรเหตุการณ์ของมัน
แนวคิดหลักที่ควรเข้าใจ:
- I/O แบบไม่บล็อก: โมดูล
httpใช้ event loop ของ Node.js ในการจัดการการเชื่อมต่อหลายรายการโดยไม่ต้องสร้างเธรดแยกสำหรับแต่ละคำขอ ซึ่งทำให้มีประสิทธิภาพสูงสำหรับการทำงานที่ขึ้นกับ I/O - สตรีม: ออบเจ็กต์คำขอและการตอบสนองสืบทอดมาจากสตรีมของ Node.js ซึ่งหมายความว่าสามารถประมวลผลข้อมูลขนาดใหญ่เป็นส่วน ๆ ได้โดยไม่ต้องโหลดทั้งหมดเข้าสู่หน่วยความจำ — สิ่งนี้มีความสำคัญสำหรับการจัดการการอัปโหลดไฟล์หรือการให้บริการไฟล์ขนาดใหญ่
- หัวข้อและรหัสสถานะ: โมดูลนี้ให้คุณควบคุม HTTP headers (
res.setHeader()) และรหัสสถานะ (res.writeHead()) ได้โดยตรง ทำให้สามารถจัดการความหมายของ HTTP ได้อย่างแม่นยำ - ไม่มีการจัดเส้นทางอัตโนมัติ: ต่างจากเฟรมเวิร์ก ที่นี่ไม่มีการจัดเส้นทางในตัว คุณต้องแยกวิเคราะห์
req.urlและreq.methodด้วยตัวเองเพื่อกำหนดวิธีตอบสนอง—ซึ่งให้ความโปร่งใสอย่างเต็มที่ว่าการจัดเส้นทางทำงานอย่างไรจริง ๆ
เมื่อคุณเรียกใช้ http.createServer(), Node.js จะสร้างเซิร์ฟเวอร์ TCP เบื้องหลังที่คอยรับการเชื่อมต่อที่เข้ามา, แยกวิเคราะห์ข้อความโปรโตคอล HTTP, และส่งเหตุการณ์ที่โค้ดของคุณสามารถจัดการได้ การเข้าใจการทำงานระดับต่ำนี้ช่วยให้คุณแก้ปัญหาและตัดสินใจด้านสถาปัตยกรรมได้อย่างมีข้อมูล
คุณสมบัติพื้นฐาน
ตรวจสอบให้แน่ใจว่าได้ติดตั้ง Node.js บนเครื่องพัฒนาของคุณแล้ว บทแนะนำนี้รองรับ Node.js 20.x หรือ 22.x LTS (แนะนำ) ในการติดตั้ง Node.js บน macOS หรือ Ubuntu ให้ทำตามขั้นตอนใน How to Install Node.js and Create a Local Development Environment on macOS หรือ How To Install Node.js on Ubuntu
แพลตฟอร์ม Node.js รองรับการสร้างเว็บเซิร์ฟเวอร์ได้โดยตรง การมีความคุ้นเคยกับพื้นฐานของ Node.js จะช่วยได้ แนะนำให้ทบทวนคู่มือของเราเกี่ยวกับวิธีการเขียนและรันโปรแกรมแรกของคุณใน Node.js
การเขียนโปรแกรมแบบอะซิงโครนัสถูกใช้ในบางส่วน หากคุณยังใหม่กับโค้ดแบบอะซิงโครนัสหรือโมดูล
fsให้ดูที่ วิธีเขียนโค้ดแบบอะซิงโครนัสใน Node.js-
สำหรับส่วนที่เน้นการผลิต ความคุ้นเคยกับการจัดการกระบวนการของ Linux (systemd/PM2) และ reverse proxy (เช่น Nginx) จะช่วยได้เมื่อใช้คำแนะนำในการปรับใช้
ขั้นตอนที่ 1 — การสร้างเซิร์ฟเวอร์ HTTP เบื้องต้น
สร้างเซิร์ฟเวอร์ HTTP แบบมินิมอลที่ตอบสนองต่อคำขอใด ๆ ด้วยข้อความธรรมดา แนวทางพื้นฐานนี้ใช้ได้ไม่ว่าคุณจะใช้ Express หรือสร้างเองตั้งแต่ต้น เมื่อ Node.js เรียก http.createServer() มันจะสร้างเซิร์ฟเวอร์ TCP ที่ฟังบนพอร์ต แต่ละการเชื่อมต่อที่เข้ามาจะกระตุ้นฟังก์ชันตัวฟังคำขอของคุณ
ตั้งค่าโฟลเดอร์โปรเจกต์ของคุณ:
mkdir first-servers
cd first-serversตอนนี้ สร้างไฟล์ที่จะเก็บโค้ด:
touch hello.jsเปิดไฟล์ในโปรแกรมแก้ไขข้อความ เราจะใช้ nano เพราะสามารถใช้งานได้ในเทอร์มินัล:
nano hello.jsเราเริ่มโดยการโหลดโมดูล http ซึ่งเป็นมาตรฐานที่มาพร้อมกับการติดตั้ง Node.js ทุกครั้ง เพิ่มบรรทัดดังต่อไปนี้เข้าไปใน hello.js:
const http = require("http");โมดูล http มีฟังก์ชันในการสร้างเซิร์ฟเวอร์ ซึ่งเราจะได้เห็นในภายหลัง หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับโมดูลใน Node.js ลองดูบทความของเราเรื่อง วิธีสร้างโมดูล Node.js
ขั้นตอนต่อไปของเราคือการกำหนดค่าคงที่สองค่า คือ โฮสต์และพอร์ตที่เซิร์ฟเวอร์ของเราจะถูกผูกไว้:
...
const host = 'localhost';
const port = 8000;ดังที่กล่าวมาก่อนหน้านี้ เว็บเซิร์ฟเวอร์จะรับคำขอจากเว็บเบราว์เซอร์และไคลเอนต์อื่นๆ เราสามารถโต้ตอบกับเว็บเซิร์ฟเวอร์ได้โดยการป้อนชื่อโดเมน ซึ่งจะถูกแปลงเป็นที่อยู่ IP โดยเซิร์ฟเวอร์ DNS ที่อยู่ IP คือชุดตัวเลขเฉพาะที่ใช้ระบุเครื่องบนเครือข่าย เช่น อินเทอร์เน็ต สำหรับข้อมูลเพิ่มเติมเกี่ยวกับแนวคิดของชื่อโดเมน สามารถดูบทความของเราเรื่อง แนะนำคำศัพท์ ส่วนประกอบ และแนวคิดของ DNS.
ค่าของ localhost เป็นที่อยู่ส่วนตัวพิเศษที่คอมพิวเตอร์ใช้เพื่ออ้างถึงตัวเอง โดยปกติจะเทียบเท่ากับที่อยู่ IP ภายใน 127.0.0.1 และสามารถใช้งานได้เฉพาะกับคอมพิวเตอร์เครื่องนั้นเท่านั้น ไม่สามารถเข้าถึงได้จากเครือข่ายท้องถิ่นที่เราเข้าร่วมหรือจากอินเทอร์เน็ต
พอร์ตเป็นตัวเลขที่เซิร์ฟเวอร์ใช้เป็นจุดสิ้นสุดหรือ "ประตู" ไปยังที่อยู่ IP ของเรา ในตัวอย่างของเรา เราจะใช้พอร์ต 8000 สำหรับเว็บเซิร์ฟเวอร์ของเรา พอร์ต 8080 และ 8000 มักถูกใช้เป็นพอร์ตเริ่มต้นในระหว่างการพัฒนา และในหลายกรณีนักพัฒนาจะใช้พอร์ตเหล่านี้แทนพอร์ตอื่นสำหรับเซิร์ฟเวอร์ HTTP
เมื่อเราผูกเซิร์ฟเวอร์ของเรากับโฮสต์และพอร์ตนี้ เราจะสามารถเข้าถึงเซิร์ฟเวอร์ของเราได้โดยการเข้าไปที่ http://localhost:8000 ในเว็บเบราว์เซอร์ภายในเครื่อง
มาสร้างฟังก์ชันพิเศษกัน ซึ่งใน Node.js เราเรียกว่า request listener ฟังก์ชันนี้ถูกออกแบบมาเพื่อจัดการคำขอ HTTP ที่เข้ามาและส่งกลับการตอบสนอง HTTP ฟังก์ชันนี้ต้องมีอาร์กิวเมนต์สองตัว คือ อ็อบเจ็กต์คำขอ (request object) และอ็อบเจ็กต์การตอบสนอง (response object) อ็อบเจ็กต์คำขอจะเก็บข้อมูลทั้งหมดของคำขอ HTTP ที่เข้ามา ส่วนอ็อบเจ็กต์การตอบสนองจะใช้ส่งกลับการตอบสนอง HTTP ให้กับเซิร์ฟเวอร์
เราต้องการให้เซิร์ฟเวอร์ตัวแรกของเราส่งข้อความนี้กลับทุกครั้งที่มีใครเข้าถึงมัน: "My first server!"
เรามาเพิ่มฟังก์ชันนั้นต่อไป:
...
const requestListener = function (req, res) {
res.writeHead(200);
res.end("My first server!");
};ฟังก์ชันมักจะถูกตั้งชื่อตามสิ่งที่มันทำ ตัวอย่างเช่น หากเราสร้างฟังก์ชันผู้ฟังคำขอเพื่อส่งคืนรายการหนังสือ เราอาจตั้งชื่อมันว่า listBooks() เนื่องจากนี่เป็นกรณีตัวอย่าง เราจะใช้ชื่อทั่วไป requestListener
ฟังก์ชันตัวฟังคำขอทั้งหมดใน Node.js รับพารามิเตอร์สองตัว: req และ res (เราสามารถตั้งชื่อแตกต่างกันได้ถ้าเราต้องการ) คำขอ HTTP ที่ผู้ใช้ส่งมาจะถูกเก็บไว้ในอ็อบเจ็กต์ Request ซึ่งสอดคล้องกับพารามิเตอร์ตัวแรก req ส่วนการตอบกลับ HTTP ที่เราส่งกลับไปยังผู้ใช้จะถูกสร้างขึ้นโดยการโต้ตอบกับอ็อบเจ็กต์ Response ในพารามิเตอร์ตัวที่สอง res
บรรทัดแรก res.writeHead(200); กำหนดรหัสสถานะ HTTP ของการตอบสนอง รหัสสถานะ HTTP แสดงว่าการร้องขอ HTTP ถูกจัดการโดยเซิร์ฟเวอร์ดีเพียงใด ในกรณีนี้ รหัสสถานะ 200 หมายถึง "OK" หากคุณสนใจที่จะเรียนรู้เกี่ยวกับรหัส HTTP ต่างๆ ที่เซิร์ฟเวอร์เว็บของคุณสามารถส่งกลับพร้อมความหมายที่พวกเขาสื่อ คำแนะนำของเราเกี่ยวกับวิธีแก้ไขปัญหารหัสข้อผิดพลาด HTTP ที่พบบ่อยเป็นจุดเริ่มต้นที่ดี
บรรทัดถัดไปของฟังก์ชัน, res.end("My first server!");, จะเขียนการตอบสนอง HTTP กลับไปยังไคลเอนต์ที่ร้องขอ ฟังก์ชันนี้จะส่งข้อมูลใด ๆ ที่เซิร์ฟเวอร์ต้องการส่งคืน ในกรณีนี้ มันกำลังส่งคืนข้อมูลข้อความ
สุดท้าย เราก็สามารถสร้างเซิร์ฟเวอร์ของเราและใช้ตัวฟังคำขอของเราได้แล้ว:
...
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});บันทึกและออกจาก nano โดยกด CTRL+X
ในบรรทัดแรก เราสร้างอ็อบเจ็กต์ server ใหม่ผ่านฟังก์ชัน createServer() ของโมดูล http เซิร์ฟเวอร์นี้รับคำขอ HTTP และส่งต่อไปยังฟังก์ชัน requestListener() ของเรา
หลังจากที่เราสร้างเซิร์ฟเวอร์ของเราแล้ว เราต้องผูกมันเข้ากับที่อยู่เครือข่าย เราทำได้โดยใช้เมธอด server.listen() ซึ่งรับอาร์กิวเมนต์สามตัวคือ port, host, และฟังก์ชัน callback ที่ทำงานเมื่อเซิร์ฟเวอร์เริ่มฟัง
อาร์กิวเมนต์ทั้งหมดนี้เป็นทางเลือก แต่เป็นความคิดที่ดีที่จะระบุอย่างชัดเจนว่าเราต้องการให้เว็บเซิร์ฟเวอร์ใช้พอร์ตและโฮสต์ใด เมื่อทำการปรับใช้เว็บเซิร์ฟเวอร์ไปยังสภาพแวดล้อมต่าง ๆ การรู้ว่าพอร์ตและโฮสต์ที่มันกำลังทำงานอยู่เป็นสิ่งจำเป็นในการตั้งค่าโหลดบาลานซิงหรือชื่อ DNS แทน
ฟังก์ชันคอลแบ็กบันทึกข้อความลงในคอนโซลของเราเพื่อให้เรารู้ว่าเซิร์ฟเวอร์เริ่มฟังการเชื่อมต่อเมื่อใด
นี่คือไฟล์ hello.js ฉบับสมบูรณ์พร้อมคำอธิบายแบบฝังในโค้ดอธิบายแต่ละส่วน:
// Import the built-in http module - no installation needed
const http = require("http");
// Define where the server will listen
const host = 'localhost'; // Only accessible from this machine
const port = 8000; // Common development port
// Request listener: handles every incoming HTTP request
const requestListener = function (req, res) {
// Set HTTP status code to 200 (OK)
res.writeHead(200);
// Send the response body and close the connection
res.end("My first server!");
};
// Create the server with our request listener
const server = http.createServer(requestListener);
// Start listening for connections
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});โค้ดนี้สร้างเซิร์ฟเวอร์ HTTP ที่ใช้งานได้จริงในไม่ถึง 20 บรรทัด ทุกครั้งที่มีไคลเอนต์เชื่อมต่อ—ไม่ว่าลิงก์หรือวิธี HTTP ใด—เซิร์ฟเวอร์จะตอบกลับด้วย “My first server!” และรหัสสถานะ 200
หมายเหตุ: แม้ว่าฟังก์ชัน requestListener() จะไม่ได้ใช้วัตถุ req แต่ก็ยังต้องเป็นอาร์กิวเมนต์ตัวแรกของฟังก์ชันอยู่ดี
ด้วยโค้ดไม่ถึงสิบห้าบรรทัด ตอนนี้เรามีเว็บเซิร์ฟเวอร์แล้ว มาดูมันทำงานและทดสอบแบบครบวงจรโดยการรันโปรแกรมกัน:
node hello.jsในคอนโซล เราจะเห็นผลลัพธ์นี้:
Server is running on http://localhost:8000สังเกตว่าพรอมต์หายไป นี่เป็นเพราะเซิร์ฟเวอร์ Node.js เป็นกระบวนการที่ทำงานยาวนาน มันจะออกจากระบบก็ต่อเมื่อพบข้อผิดพลาดที่ทำให้เกิดการล่มและหยุดทำงาน หรือถ้าเราหยุดกระบวนการ Node.js ที่รันเซิร์ฟเวอร์อยู่
ในหน้าต่างเทอร์มินอลแยกต่างหาก เราจะสื่อสารกับเซิร์ฟเวอร์โดยใช้ cURL เครื่องมือ CLI สำหรับส่งและรับข้อมูลผ่านเครือข่าย พิมพ์คำสั่งเพื่อส่งคำขอ HTTP GET ไปยังเซิร์ฟเวอร์ที่กำลังทำงานของเรา:
curl http://localhost:8000เมื่อเรากด ENTER เทอร์มินัลของเราจะแสดงผลลัพธ์ดังต่อไปนี้:
My first server!ตอนนี้เราตั้งค่าเซิร์ฟเวอร์เรียบร้อยแล้วและได้รับการตอบสนองจากเซิร์ฟเวอร์ครั้งแรกของเรา
มาลองมาดูกันว่าเกิดอะไรขึ้นเมื่อเราทดสอบเซิร์ฟเวอร์ของเรา โดยใช้ cURL เราได้ส่งคำขอ GET ไปยังเซิร์ฟเวอร์ที่ http://localhost:8000 เซิร์ฟเวอร์ Node.js ของเราฟังการเชื่อมต่อจากที่อยู่นั้น เซิร์ฟเวอร์ส่งคำขอนั้นไปยังฟังก์ชัน requestListener() ฟังก์ชันส่งกลับข้อมูลข้อความพร้อมรหัสสถานะ 200 จากนั้นเซิร์ฟเวอร์ส่งการตอบสนองนั้นกลับไปยัง cURL ซึ่งจะแสดงข้อความในเทอร์มินัลของเรา
ในการใช้งานจริง คุณมักจะไม่ผูกกับ localhost แต่จะผูกกับ 0.0.0.0 เพื่อรับการเชื่อมต่อจากทุกอินเทอร์เฟซเครือข่าย อย่างไรก็ตาม สำหรับการพัฒนาในเครื่อง localhost จะปลอดภัยกว่าและป้องกันการเข้าถึงจากภายนอกเซิร์ฟเวอร์พัฒนาของคุณ นอกจากนี้ หากพอร์ต 8000 ถูกใช้งานอยู่แล้ว คุณจะได้รับข้อผิดพลาด EADDRINUSE—เพียงแค่เลือกพอร์ตอื่นหรือหยุดกระบวนการที่ใช้พอร์ตนั้นด้วย lsof -i :8000 (macOS/Linux)
ก่อนที่เราจะดำเนินการต่อ ลองออกจากเซิร์ฟเวอร์ที่กำลังรันอยู่ของเราด้วยการกด CTRL+C การกระทำนี้จะหยุดการทำงานของเซิร์ฟเวอร์ ทำให้เรากลับมาที่พรอมต์คำสั่ง
ในเว็บไซต์ส่วนใหญ่ที่เราเยี่ยมชมหรือ API ที่เราใช้ การตอบสนองจากเซิร์ฟเวอร์มักไม่ใช่ข้อความธรรมดา เราจะได้รับหน้า HTML และข้อมูล JSON เป็นรูปแบบการตอบสนองทั่วไป ในขั้นตอนต่อไป เราจะได้เรียนรู้วิธีการส่งกลับการตอบสนอง HTTP ในรูปแบบข้อมูลที่พบบ่อยบนเว็บ
ถ้าคุณต้องการเรียนรู้เกี่ยวกับการส่งคำขอ HTTP จาก Node.js (ฝั่งไคลเอนต์) ลองดูคู่มือตามหัวข้อของเราว่า วิธีสร้าง HTTP Client ด้วย Core HTTP ใน Node.js.
ขั้นตอนที่ 2 — การส่งคืนประเภทเนื้อหาที่แตกต่างกัน
การตอบกลับที่เราส่งจากเว็บเซิร์ฟเวอร์สามารถมีหลายรูปแบบ JSON และ HTML เคยถูกกล่าวถึงก่อนหน้านี้ และเรายังสามารถส่งรูปแบบข้อความอื่นๆ เช่น XML และ CSV สุดท้าย เว็บเซิร์ฟเวอร์สามารถส่งข้อมูลที่ไม่ใช่ข้อความได้ เช่น PDF ไฟล์ซิป เสียง และวิดีโอ
ในบทความนี้ นอกเหนือจากข้อความธรรมดาที่เราเพิ่งส่งคืนไป คุณจะได้เรียนรู้วิธีการส่งคืนข้อมูลประเภทต่อไปนี้:
- เจสัน
- CSV
- เอชทีเอ็มแอล
ข้อมูลทั้งสามประเภทเป็นข้อมูลแบบข้อความ และเป็นรูปแบบที่นิยมใช้ในการส่งมอบเนื้อหาบนเว็บ ภาษาและเครื่องมือในการพัฒนาเซิร์ฟเวอร์หลายตัวรองรับการส่งคืนข้อมูลประเภทต่างๆ เหล่านี้ ในบริบทของ Node.js เราจำเป็นต้องทำสองสิ่งดังนี้:
- ตั้งค่าเฮดเดอร์
Content-Typeในการตอบสนอง HTTP ของเราด้วยค่าที่เหมาะสม - ตรวจสอบให้แน่ใจว่า
res.end()ได้รับข้อมูลในรูปแบบที่ถูกต้อง
มาดูสิ่งนี้ในการปฏิบัติด้วยตัวอย่างกัน โค้ดที่เราจะเขียนในส่วนนี้และส่วนถัดไปมีความคล้ายคลึงกับโค้ดที่เราเขียนก่อนหน้านี้ ส่วนใหญ่การเปลี่ยนแปลงจะอยู่ภายในฟังก์ชัน requestListener() มาเริ่มสร้างไฟล์ด้วย “โค้ดแม่แบบ” นี้เพื่อให้ส่วนถัดไปติดตามได้ง่ายขึ้น
สร้างไฟล์ใหม่ชื่อ html.js ไฟล์นี้จะถูกใช้ในภายหลังเพื่อส่งข้อความ HTML ในการตอบสนอง HTTP เราจะใส่โค้ดเทมเพลตที่นี่และคัดลอกไปยังเซิร์ฟเวอร์อื่น ๆ ที่ส่งค่าประเภทต่าง ๆ
ในเทอร์มินัล ให้พิมพ์คำสั่งดังต่อไปนี้:
touch html.jsตอนนี้เปิดไฟล์นี้ในโปรแกรมแก้ไขข้อความ:
nano html.jsเรามาคัดลอก “รหัสเทมเพลต” กัน ใส่อันนี้ใน nano:
const http = require("http");
const host = 'localhost';
const port = 8000;
const requestListener = function (req, res) {};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});บันทึกและออกจาก html.js ด้วย CTRL+X จากนั้นกลับไปที่เทอร์มินัล
ตอนนี้มาคัดลอกไฟล์นี้ไปยังไฟล์ใหม่สองไฟล์ ไฟล์แรกจะใช้สำหรับส่งคืนข้อมูล CSV ในการตอบกลับ HTTP:
cp html.js csv.jsไฟล์ที่สองจะส่งกลับการตอบสนองเป็น JSON บนเซิร์ฟเวอร์:
cp html.js json.jsไฟล์ที่เหลือจะใช้สำหรับแบบฝึกหัดในภายหลัง:
cp html.js htmlFile.js
cp html.js routes.jsตอนนี้เราพร้อมที่จะดำเนินการฝึกต่อแล้ว มาลองเริ่มด้วยการส่งกลับ JSON กันเถอะ
ให้บริการ JSON
JavaScript Object Notation หรือที่มักเรียกกันว่า JSON เป็นรูปแบบการแลกเปลี่ยนข้อมูลที่ใช้ข้อความเป็นฐาน ตามชื่อของมัน JSON ได้มาจากอ็อบเจ็กต์ของ JavaScript แต่เป็นอิสระจากภาษา ซึ่งหมายความว่าสามารถใช้ได้กับทุกภาษาโปรแกรมที่สามารถวิเคราะห์ไวยากรณ์ของมันได้
JSON มักถูกใช้โดย API เพื่อรับและส่งข้อมูล ความนิยมของมันมาจากขนาดการถ่ายโอนข้อมูลที่เล็กกว่ามาตรฐานการแลกเปลี่ยนข้อมูลก่อนหน้านี้เช่น XML รวมถึงเครื่องมือที่มีอยู่ที่ช่วยให้โปรแกรมสามารถแยกวิเคราะห์ข้อมูลเหล่านี้ได้โดยไม่ต้องใช้ความพยายามมาก หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับ JSON คุณสามารถอ่านคู่มือของเราเกี่ยวกับวิธีการใช้งาน JSON ใน JavaScript
เปิดไฟล์ json.js ด้วย nano:
nano json.jsเมื่อให้บริการ JSON คุณต้องตั้งค่าเฮดเดอร์ Content-Type เป็น application/json หากไม่ทำ เบราว์เซอร์และไคลเอนต์ API จะจัดการการตอบสนองเป็นข้อความธรรมดา ทำให้ fetch().json() ล้มเหลว ตั้งค่าเฮดเดอร์ก่อนเรียกใช้ writeHead()—เมื่อเฮดเดอร์ถูกส่งไปแล้ว จะไม่สามารถแก้ไขได้
ข้อผิดพลาดที่พบบ่อย: การเรียก res.writeHead(200) ก่อน res.setHeader() จะทำให้เฮดเดอร์ถูกล็อก หากคุณพยายามตั้งค่าเฮดเดอร์หลังจาก writeHead() มันจะถูกละเลยเสมอ ให้เรียก setHeader() ก่อนเสมอ แล้วค่อยเรียก writeHead()
แก้ไขฟังก์ชัน requestListener():
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(`{"message": "This is a JSON response"}`);
};
...รหัสสถานะ 200 แสดงถึงความสำเร็จ ตัวเนื้อหาการตอบกลับมี JSON ที่ถูกต้อง
ทำไมต้องใช้ JSON.stringify() แทนการเขียนสตริงด้วยตนเอง? การเขียน JSON ด้วยตนเองมีความเสี่ยงต่อการเกิดข้อผิดพลาด หากข้อความของคุณมีเครื่องหมายคำพูด, ขึ้นบรรทัดใหม่ หรืออักขระพิเศษ คุณต้องทำการหนีอักขระเหล่านี้ด้วยตนเอง JSON.stringify() จะจัดการสิ่งนี้โดยอัตโนมัติ:
const data = {
message: "This is a JSON response",
timestamp: Date.now(),
note: 'He said "hello"' // Quotes automatically escaped
};
res.setHeader("Content-Type", "application/json");
res.writeHead(200);
res.end(JSON.stringify(data));ระวังการอ้างอิงแบบวนซ้ำ: หากวัตถุของคุณอ้างอิงตัวเอง, JSON.stringify() จะเกิดข้อผิดพลาด: TypeError: Converting circular structure to JSON. สิ่งนี้มักเกิดขึ้นกับผลลัพธ์จากการค้นหาฐานข้อมูล. ควรตรวจสอบโครงสร้างข้อมูลของคุณเสมอ.
จะเกิดอะไรขึ้นถ้าคุณพยายามส่งอ็อบเจ็กต์โดยตรง? โมดูล http รับเฉพาะสตริงหรือบัฟเฟอร์เท่านั้น ถ้าคุณลอง res.end({ message: "test" }) Node.js จะเกิดข้อผิดพลาด: TypeError [ERR_INVALID_ARG_TYPE]: อาร์กิวเมนต์ตัวแรกต้องเป็นสตริงหรือบัฟเฟอร์ คุณต้องแปลงอ็อบเจ็กต์เป็นสตริง JSON ก่อน
ตรวจสอบว่าหัวข้อถูกตั้งค่าอย่างถูกต้องหรือไม่: ใช้ -i กับ curl เพื่อดูหัวข้อการตอบสนอง:
curl -i http://localhost:8000คุณควรจะเห็น Content-Type: application/json ในส่วนหัว (headers) หากคุณเห็น text/plain หรือไม่มี Content-Type แสดงว่าคุณอาจเรียกใช้ setHeader() หลังจาก writeHead() หรืออาจลืมเรียกใช้เลย
สถานการณ์การดีบักที่พบได้บ่อย: ถ้า JavaScript ฝั่งหน้าเว็บของคุณได้รับค่า undefined เมื่อเรียกใช้ fetch('/api').then(r => r.json()) ให้ตรวจสอบเฮดเดอร์ด้วย curl -i โดย Content-Type ต้องเป็น application/json อย่างแม่นยำ (ตัวพิมพ์ใหญ่เล็กมีผลสำหรับ parser บางตัวที่เข้มงวด)
บันทึกและออกจาก json.js โดยกด CTRL+X ตอนนี้ มารันเซิร์ฟเวอร์ด้วยคำสั่ง node กัน:
node json.jsในเทอร์มินอลอีกตัว ให้เราเข้าถึงเซิร์ฟเวอร์โดยใช้ cURL:
curl http://localhost:8000เมื่อเรากด ENTER เราจะเห็นผลลัพธ์ดังต่อไปนี้:
{"message": "This is a JSON response"}ตอนนี้เราสามารถส่งกลับการตอบสนองแบบ JSON ได้สำเร็จแล้ว เหมือนกับ API ยอดนิยมหลายๆ ตัวที่เราใช้สร้างแอป อย่าลืมออกจากเซิร์ฟเวอร์ที่กำลังรันอยู่ด้วย CTRL+C เพื่อที่เราจะได้กลับไปยังพรอมต์เทอร์มินอลปกติ ต่อไป มาลองดูอีกหนึ่งรูปแบบยอดนิยมในการส่งกลับข้อมูลกัน: CSV
การให้บริการ CSV
รูปแบบไฟล์ Comma Separated Values (CSV) เป็นมาตรฐานข้อความที่มักใช้สำหรับการจัดข้อมูลในรูปแบบตาราง ในหลายกรณี แต่ละแถวจะแยกจากกันด้วยการขึ้นบรรทัดใหม่ และแต่ละรายการในแถวจะแยกจากกันด้วยเครื่องหมายจุลภาค
ในพื้นที่ทำงานของเรา ให้เปิดไฟล์ csv.js ด้วยตัวแก้ไขข้อความ:
nano csv.jsมาลองเพิ่มบรรทัดต่อไปนี้ลงในฟังก์ชัน requestListener() ของเรากันเถอะ:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
};
...ครั้งนี้ Content-Type ของเราบอกว่าได้มีการส่งไฟล์ CSV กลับมาเนื่องจากค่าคือ text/csv ส่วนหัวข้อที่สองที่เราเพิ่มคือ Content-Disposition ส่วนหัวข้อนี้บอกเบราว์เซอร์ว่าจะแสดงข้อมูลอย่างไร โดยเฉพาะในเบราว์เซอร์หรือเป็นไฟล์แยกต่างหาก
เมื่อเราส่งตอบกลับในรูปแบบ CSV เบราว์เซอร์สมัยใหม่ส่วนใหญ่จะดาวน์โหลดไฟล์โดยอัตโนมัติ แม้ว่าจะไม่ได้ตั้งค่าเฮดเดอร์ Content-Disposition ก็ตาม อย่างไรก็ตาม เมื่อส่งคืนไฟล์ CSV เรายังคงควรเพิ่มเฮดเดอร์นี้เนื่องจากช่วยให้เรากำหนดชื่อไฟล์ CSV ได้ ในกรณีนี้ เราจะบอกเบราว์เซอร์ว่าไฟล์ CSV นี้เป็นไฟล์แนบและควรดาวน์โหลด จากนั้นเราก็แจ้งให้เบราว์เซอร์ทราบว่าชื่อไฟล์คือ oceanpals.csv
มาเขียนข้อมูล CSV ในการตอบสนอง HTTP กันเถอะ:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
res.writeHead(200);
res.end(`id,name,email\n1,Sammy Shark,shark@ocean.com`);
};
...ส่งสถานะ 200/OK พร้อมข้อมูล CSV เครื่องหมายจุลภาคจะแยกค่าต่าง ๆ ในแต่ละคอลัมน์ และอักขระขึ้นบรรทัดใหม่ (\n) จะแยกแต่ละแถว เรามีสองแถว: แถวหัวข้อและแถวข้อมูลหนึ่งแถว
ข้อควรระวังเรื่องการจัดรูปแบบ CSV: ตัวอย่างง่ายๆ ข้างต้นทำงานได้ แต่การสร้าง CSV ในการใช้งานจริงต้องมีการหนีอักขระ หากข้อมูลของคุณมีเครื่องหมายจุลภาค (เช่น "Smith, John") หรือเครื่องหมายคำพูด จะทำให้รูปแบบ CSV เสีย การหนีอักขระ CSV ที่ถูกต้องคือการห่อค่าด้วยเครื่องหมายคำพูดและเพิ่มเครื่องหมายคำพูดภายในเป็นสองเท่า: "Smith, John" จะกลายเป็น "Smith, ""John""" ใน CSV สำหรับการใช้งานจริง ควรใช้ไลบรารีเช่น csv-stringify หรือหนีอักขระค่าอย่างถูกต้องเพื่อป้องกันข้อผิดพลาดในการแยกวิเคราะห์ข้อมูล
จะเกิดอะไรขึ้นถ้าไม่มี Content-Disposition? เบราว์เซอร์ส่วนใหญ่จะดาวน์โหลดไฟล์ถ้า Content-Type: text/csv ถูกตั้งค่า แต่ชื่อไฟล์อาจเป็นเส้นทาง URL หรือชื่อทั่วไป attachment;filename=oceanpals.csv เฮดเดอร์จะช่วยให้เบราว์เซอร์บันทึกไฟล์ด้วยชื่อเฉพาะ หากไม่มี attachment บางเบราว์เซอร์จะพยายามแสดง CSV แบบอินไลน์ ซึ่งมักจะดูรกตา
เราจะทดสอบเซิร์ฟเวอร์นี้ในเบราว์เซอร์ บันทึก csv.js แล้วออกจากโปรแกรมแก้ไขด้วย CTRL+X
รันเซิร์ฟเวอร์ด้วยคำสั่ง Node.js:
node csv.jsใน Terminal อีกตัวหนึ่ง มาเชื่อมต่อไปยังเซิร์ฟเวอร์โดยใช้ cURL:
curl http://localhost:8000คอนโซลจะแสดงดังนี้:
id,name,email
1,Sammy Shark,shark@ocean.comถ้าเราไปที่ http://localhost:8000 ในเบราว์เซอร์ของเรา ไฟล์ CSV จะถูกดาวน์โหลด ชื่อไฟล์จะเป็น oceanpals.csv
ออกจากเซิร์ฟเวอร์ที่กำลังทำงานด้วย CTRL+C เพื่อกลับไปที่พรอมต์ของเทอร์มินัลมาตรฐาน
หลังจากที่เราได้ส่งคืน JSON และ CSV เราได้ครอบคลุมสองกรณีที่ได้รับความนิยมสำหรับ API แล้ว ตอนนี้มาดูกันว่าการส่งคืนข้อมูลสำหรับเว็บไซต์ที่ผู้คนเข้าชมในเบราว์เซอร์ทำได้อย่างไร
การให้บริการ HTML
HTML (HyperText Markup Language) เป็นรูปแบบที่ใช้กันทั่วไปที่สุดเมื่อเราต้องการให้ผู้ใช้โต้ตอบกับเซิร์ฟเวอร์ของเราผ่านเว็บเบราว์เซอร์ มันถูกสร้างขึ้นมาเพื่อจัดโครงสร้างเนื้อหาเว็บ เว็บเบราว์เซอร์ถูกสร้างขึ้นมาเพื่อแสดงเนื้อหา HTML รวมถึงสไตล์ใดๆ ที่เราเพิ่มด้วย CSS ซึ่งเป็นเทคโนโลยีเว็บฝั่งหน้าอีกตัวหนึ่งที่ช่วยให้เราสามารถเปลี่ยนแปลงรูปลักษณ์ของเว็บไซต์ของเราได้
มาเปิดไฟล์ html.js อีกครั้งด้วยโปรแกรมแก้ไขข้อความของเรากัน:
nano html.jsแก้ไขฟังก์ชัน requestListener() เพื่อส่งค่าเฮดเดอร์ Content-Type ที่เหมาะสมสำหรับการตอบสนองเป็น HTML:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/html");
};
...ตอนนี้ มาลองส่งเนื้อหา HTML กลับไปยังผู้ใช้กัน เพิ่มบรรทัดที่ไฮไลต์ลงใน html.js ให้ดูเหมือนดังนี้:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(`<html><body><h1>This is HTML</h1></body></html>`);
};
...เราจะเริ่มด้วยการเพิ่มรหัสสถานะ HTTP จากนั้นเราจะเรียกใช้ response.end() พร้อมอาร์กิวเมนต์เป็นสตริงที่มี HTML ที่ถูกต้อง เมื่อเราเข้าถึงเซิร์ฟเวอร์ของเราในเบราว์เซอร์ เราจะเห็นหน้า HTML ที่มีแท็กหัวข้อหนึ่งแท็กซึ่งมี This is HTML
ให้เราบันทึกและออกโดยกด CTRL+X ตอนนี้ มารันเซิร์ฟเวอร์ด้วยคำสั่ง node:
node html.jsเราจะเห็น Server is running on http://localhost:8000 เมื่อโปรแกรมของเราเริ่มทำงานแล้ว
ตอนนี้ไปที่เบราว์เซอร์และเข้าเว็บไซต์ http://localhost:8000 หน้าของเราจะมีลักษณะดังนี้:
ให้หยุดเซิร์ฟเวอร์ที่กำลังรันอยู่ด้วย CTRL+C และกลับไปที่พรอมต์เทอร์มินัลปกติ
เป็นเรื่องปกติที่ HTML จะถูกเขียนในไฟล์ แยกออกมาจากโค้ดฝั่งเซิร์ฟเวอร์ เช่น โปรแกรม Node.js ของเรา ต่อไปมาดูกันว่าเราสามารถส่งกลับ HTML จากไฟล์ได้อย่างไรบ้าง
ขั้นตอนที่ 3 — การให้บริการหน้า HTML จากไฟล์
เราสามารถให้บริการ HTML เป็นสตริงใน Node.js แก่ผู้ใช้ได้ แต่สิ่งที่แนะนำมากกว่าคือการโหลดไฟล์ HTML และให้บริการเนื้อหาของมัน ด้วยวิธีนี้ เมื่อไฟล์ HTML มีขนาดใหญ่ขึ้น เราไม่ต้องดูแลสตริงยาวๆ ในโค้ด Node.js ของเรา ทำให้โค้ดกระชับขึ้นและช่วยให้เราสามารถทำงานในแต่ละส่วนของเว็บไซต์ได้อย่างอิสระ แนวคิด "การแยกความรับผิดชอบ" นี้เป็นเรื่องปกติในหลายการตั้งค่าการพัฒนาเว็บ ดังนั้นจึงเป็นเรื่องดีที่ควรรู้วิธีโหลดไฟล์ HTML เพื่อสนับสนุนสิ่งนี้ใน Node.js
เพื่อให้บริการไฟล์ HTML เราจะโหลดไฟล์ HTML ด้วยโมดูล fs และใช้ข้อมูลของมันเมื่อเขียนการตอบสนอง HTTP ของเรา
ก่อนอื่น เราจะสร้างไฟล์ HTML ที่เว็บเซิร์ฟเวอร์จะส่งกลับ สร้างไฟล์ HTML ใหม่:
touch index.htmlตอนนี้เปิด index.html ในโปรแกรมแก้ไขข้อความ:
nano index.htmlหน้าเว็บของเราจะเรียบง่าย มีพื้นหลังสีส้ม และจะแสดงข้อความทักทายในกลางหน้าจอ เพิ่มโค้ดนี้ลงในไฟล์:
<!DOCTYPE html>
<head>
<title>My Website</title>
<style>
*,
html {
margin: 0;
padding: 0;
border: 0;
}
html {
width: 100%;
height: 100%;
}
body {
width: 100%;
height: 100%;
position: relative;
background-color: rgb(236, 152, 42);
}
.center {
width: 100%;
height: 50%;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-family: "Trebuchet MS", Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 144px;
}
p {
font-size: 64px;
}
</style>
</head>
<body>
<div class="center">
<h1>Hello Again!</h1>
<p>This is served from a file</p>
</div>
</body>
</html>หน้าเว็บเพจเดียวนี้แสดงสองบรรทัดของข้อความ: Hello Again! และ This is served from a file ข้อความทั้งสองบรรทัดปรากฏอยู่ตรงกลางหน้า โดยเรียงกันจากบนลงล่าง บรรทัดแรกของข้อความจะแสดงเป็นหัวเรื่อง ซึ่งหมายความว่าขนาดจะใหญ่ขึ้น ส่วนบรรทัดที่สองของข้อความจะแสดงขนาดเล็กกว่านิดหน่อย ข้อความทั้งหมดจะแสดงเป็นสีขาว และหน้าเว็บมีพื้นหลังสีส้ม
แม้ว่านี่จะไม่ใช่เนื้อหาหลักของบทความหรือชุดบทความนี้ แต่ถ้าคุณสนใจที่จะเรียนรู้เพิ่มเติมเกี่ยวกับ HTML, CSS และเทคโนโลยีเว็บด้านหน้าอื่น ๆ คุณสามารถดูคู่มือ Getting Started with the Web ของ Mozilla ได้
นั่นคือทั้งหมดที่เราต้องการสำหรับ HTML ดังนั้นให้บันทึกและออกจากไฟล์ด้วย CTRL+X ตอนนี้เราสามารถไปต่อที่โค้ดเซิร์ฟเวอร์ได้แล้ว
สำหรับแบบฝึกหัดนี้ เราจะทำงานกับ htmlFile.js เปิดไฟล์ด้วยโปรแกรมแก้ไขข้อความ:
nano htmlFile.jsเนื่องจากเราต้องอ่านไฟล์ เรามาเริ่มโดยการนำเข้าโมดูล fs กันเถอะ:
const http = require("http");
const fs = require('fs').promises;
...โมดูลนี้มีฟังก์ชัน readFile() ที่เราจะใช้เพื่อโหลดไฟล์ HTML ในตำแหน่งนั้น เรานำเข้าส่วนที่เป็น promise เพื่อให้สอดคล้องกับแนวทางปฏิบัติที่ดีที่สุดของ JavaScript สมัยใหม่ เราใช้ promises เพราะไวยากรณ์ของมันกระชับกว่าการใช้ callbacks ซึ่งเราจำเป็นต้องใช้หากเรากำหนด fs แค่ require('fs') หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับแนวทางปฏิบัติที่ดีที่สุดในการเขียนโปรแกรมแบบอะซิงโครนัส คุณสามารถอ่านคู่มือวิธีการเขียนโค้ดอะซิงโครนัสใน Node.js ของเราได้
เราต้องการให้ไฟล์ HTML ของเราถูกอ่านเมื่อผู้ใช้ร้องขอระบบของเรา มาเริ่มโดยการแก้ไข requestListener() ให้สามารถอ่านไฟล์ได้:
...
const requestListener = function (req, res) {
fs.readFile(__dirname + "/index.html")
};
...เราใช้วิธี fs.readFile() เพื่อโหลดไฟล์ อาร์กิวเมนต์ของมันคือ __dirname + "/index.html" ตัวแปรพิเศษ __dirname มีเส้นทางแบบสมบูรณ์ของตำแหน่งที่โค้ด Node.js ถูกเรียกใช้ จากนั้นเราจึงต่อ /index.html เพื่อให้สามารถโหลดไฟล์ HTML ที่เราสร้างไว้ก่อนหน้านี้ได้
ตอนนี้ให้เราคืนค่าเพจ HTML หลังจากที่โหลดเสร็จแล้ว:
...
const requestListener = function (req, res) {
fs.readFile(__dirname + "/index.html")
.then(contents => {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(contents);
})
};
...หากคำสัญญา fs.readFile() สำเร็จ จะส่งข้อมูลกลับมา เราใช้เมธอด then() เพื่อจัดการกับกรณีนี้ พารามิเตอร์ contents จะมีข้อมูลของไฟล์ HTML
เรากำหนดค่าเฮดเดอร์ Content-Type เป็น text/html ก่อน เพื่อบอกกับไคลเอนต์ว่าเรากำลังส่งข้อมูล HTML จากนั้นเราก็เขียนรหัสสถานะเพื่อระบุว่าการร้องขอประสบความสำเร็จ สุดท้ายเราส่งหน้า HTML ที่เราโหลดไปยังไคลเอนต์ โดยใช้ข้อมูลที่อยู่ในตัวแปร contents
วิธีการ fs.readFile() อาจล้มเหลวได้บางครั้ง ดังนั้นเราควรจัดการกรณีนี้เมื่อเกิดข้อผิดพลาด เพิ่มสิ่งนี้เข้าไปในฟังก์ชัน requestListener():
...
const requestListener = function (req, res) {
fs.readFile(__dirname + "/index.html")
.then(contents => {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(contents);
})
.catch(err => {
res.writeHead(500);
res.end('Internal Server Error');
return;
});
};
...บันทึกไฟล์และออกจาก nano ด้วย CTRL+X
เมื่อคำสัญญาพบข้อผิดพลาด มันจะถูกปฏิเสธ เราจัดการกรณีนั้นด้วยเมธอด catch() ซึ่งจะตั้งรหัสสถานะเป็น 500 เพื่อบ่งบอกว่าพบข้อผิดพลาดภายใน และส่งข้อความแสดงข้อผิดพลาดที่เข้าใจง่ายสำหรับผู้ใช้ ในการใช้งานจริง คุณไม่ควรเปิดเผยรายละเอียดข้อผิดพลาดดิบให้ผู้ใช้เห็น เพราะอาจรั่วไหลข้อมูลสำคัญได้
รันเซิร์ฟเวอร์ของเราด้วยคำสั่ง node:
node htmlFile.jsในเว็บเบราว์เซอร์ ให้ไปที่ http://localhost:8000 คุณจะเห็นหน้าดังนี้:
ตอนนี้คุณได้ส่งหน้า HTML จากเซิร์ฟเวอร์ไปยังผู้ใช้แล้ว คุณสามารถออกจากเซิร์ฟเวอร์ที่กำลังทำงานได้ด้วย CTRL+C คุณจะเห็นพรอมต์ของเทอร์มินัลกลับมาหลังจากที่คุณทำเช่นนั้น
เมื่อเขียนโค้ดแบบนี้ในระบบจริง คุณอาจจะไม่อยากโหลดหน้า HTML ทุกครั้งที่มีการรับคำขอ HTTP แม้ว่าหน้า HTML นี้จะมีขนาดประมาณ 800 ไบต์ แต่เว็บไซต์ที่ซับซ้อนกว่านั้นอาจมีขนาดเป็นเมกะไบต์ ไฟล์ขนาดใหญ่สามารถใช้เวลานานในการโหลด หากเว็บไซต์ของคุณคาดว่าจะมีการเข้าชมจำนวนมาก อาจจะดีที่สุดในการโหลดไฟล์ HTML ตั้งแต่เริ่มต้นและเก็บเนื้อหาของมันไว้ หลังจากที่โหลดเสร็จแล้ว คุณก็สามารถตั้งค่าเซิร์ฟเวอร์และให้มันฟังคำขอบนที่อยู่ที่กำหนดได้
เพื่อสาธิตวิธีนี้ มาดูว่าพวกเราสามารถปรับปรุงเซิร์ฟเวอร์ของเราให้มีประสิทธิภาพและสามารถปรับขนาดได้ดีขึ้นอย่างไร
การให้บริการ HTML อย่างมีประสิทธิภาพ
แทนที่จะโหลด HTML สำหรับทุกคำขอ ในขั้นตอนนี้เราจะโหลดเพียงครั้งเดียวตอนเริ่มต้น คำขอจะส่งข้อมูลที่เราโหลดตอนเริ่มต้นกลับมา
ในเทอร์มินัล ให้เปิดสคริปต์ Node.js อีกครั้งด้วยโปรแกรมแก้ไขข้อความ:
nano htmlFile.jsเรามาเริ่มต้นด้วยการเพิ่มตัวแปรใหม่ก่อนที่เราจะสร้างฟังก์ชัน requestListener():
...
let indexFile;
const requestListener = function (req, res) {
...เมื่อเราเรียกใช้โปรแกรมนี้ ตัวแปรนี้จะเก็บเนื้อหาของไฟล์ HTML
ตอนนี้ มาปรับฟังก์ชัน requestListener() กันใหม่ แทนที่จะโหลดไฟล์ ฟังก์ชันจะส่งค่าข้อมูลของ indexFile แทน:
...
const requestListener = function (req, res) {
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(indexFile);
};
...ต่อไป เราจะย้ายตรรกะการอ่านไฟล์จากฟังก์ชัน requestListener() ไปยังการเริ่มต้นเซิร์ฟเวอร์ของเรา ทำการเปลี่ยนแปลงดังต่อไปนี้เมื่อเราสร้างเซิร์ฟเวอร์:
...
const server = http.createServer(requestListener);
fs.readFile(__dirname + "/index.html")
.then(contents => {
indexFile = contents;
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});
})
.catch(err => {
console.error(`Could not read index.html file: ${err}`);
process.exit(1);
});บันทึกไฟล์และออกจาก nano ด้วย CTRL+X
โค้ดที่อ่านไฟล์นั้นคล้ายกับสิ่งที่เราเขียนในความพยายามครั้งแรก อย่างไรก็ตาม เมื่อเราอ่านไฟล์สำเร็จแล้ว เราจะบันทึกเนื้อหาไปยังตัวแปรทั่วโลก indexFile ของเรา จากนั้นเราจะเริ่มเซิร์ฟเวอร์ด้วยเมธอด listen() สิ่งสำคัญคือไฟล์จะถูกโหลดก่อนที่เซิร์ฟเวอร์จะทำงาน ด้วยวิธีนี้ ฟังก์ชัน requestListener() จะมั่นใจได้ว่าจะส่งคืนหน้า HTML เพราะ indexFile ไม่ใช่ตัวแปรว่างอีกต่อไป
ตัวจัดการข้อผิดพลาดของเราก็เปลี่ยนแปลงเช่นกัน หากไม่สามารถโหลดไฟล์ได้ เราจะจับข้อผิดพลาดและพิมพ์ลงในคอนโซลของเรา จากนั้นเราจะออกจากโปรแกรม Node.js ด้วยฟังก์ชัน exit() โดยไม่เริ่มเซิร์ฟเวอร์ ด้วยวิธีนี้เราสามารถเห็นว่าเหตุใดการอ่านไฟล์จึงล้มเหลว แก้ไขปัญหา และเริ่มเซิร์ฟเวอร์อีกครั้ง
ตอนนี้เราได้สร้างเว็บเซิร์ฟเวอร์ต่างๆ ที่ส่งข้อมูลหลายประเภทกลับไปยังผู้ใช้แล้ว จนถึงตอนนี้ เรายังไม่ได้ใช้ข้อมูลการร้องขอใดๆ เพื่อกำหนดว่าจะส่งอะไรกลับไป เมื่อเราต้องตั้งค่าเส้นทางหรือ path ต่างๆ ในเซิร์ฟเวอร์ Node.js เราจะต้องใช้ข้อมูลการร้องขอ ดังนั้นต่อไปมาดูกันว่ามันทำงานร่วมกันอย่างไร
ขั้นตอนที่ 4 — การจัดการเส้นทางโดยใช้วัตถุคำขอ HTTP
เว็บไซต์ส่วนใหญ่ที่เราเยี่ยมชมหรือ API ที่เราใช้มักจะมีมากกว่าหนึ่งจุดเชื่อมต่อเพื่อให้เราสามารถเข้าถึงทรัพยากรต่าง ๆ ตัวอย่างที่ดีคือระบบการจัดการหนังสือ ระบบหนึ่งที่อาจใช้ในห้องสมุด ซึ่งจะไม่เพียงแต่จัดการข้อมูลหนังสือ แต่ยังจัดการข้อมูลผู้แต่งเพื่อความสะดวกในการทำดรรชนีและการค้นหาอีกด้วย
แม้ว่าข้อมูลเกี่ยวกับหนังสือและผู้เขียนจะเกี่ยวข้องกัน แต่พวกมันเป็นวัตถุที่แตกต่างกัน ในกรณีเหล่านี้ นักพัฒนาซอฟต์แวร์มักจะเขียนโค้ดให้แต่ละวัตถุอยู่บนเอนด์พอยต์ที่ต่างกัน เพื่อบอกให้ผู้ใช้ API ทราบว่าพวกเขากำลังโต้ตอบกับข้อมูลประเภทใด
มาสร้างเซิร์ฟเวอร์ใหม่สำหรับห้องสมุดขนาดเล็ก ซึ่งจะส่งคืนข้อมูลสองประเภทที่แตกต่างกัน หากผู้ใช้ไปที่ที่อยู่ของเซิร์ฟเวอร์ของเราที่ /books พวกเขาจะได้รับรายการหนังสือในรูปแบบ JSON หากไปที่ /authors พวกเขาจะได้รับรายการข้อมูลผู้เขียนในรูปแบบ JSON
จนถึงตอนนี้ เซิร์ฟเวอร์ส่งกลับการตอบสนองแบบเดียวกันสำหรับทุกคำขอ เพื่อรองรับหลายแหล่งข้อมูล ให้เก็บข้อมูลของคุณในตัวแปรและใช้การกำหนดเส้นทางตามเงื่อนไข ตัวอย่างเช่น:
const books = JSON.stringify([
{ title: "The Alchemist", author: "Paulo Coelho", year: 1988 },
{ title: "The Prophet", author: "Kahlil Gibran", year: 1923 }
]);
const authors = JSON.stringify([
{ name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 },
{ name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 }
]);
const requestListener = function (req, res) {
res.setHeader("Content-Type", "application/json");
switch (req.url) {
case "/books":
res.writeHead(200);
res.end(books);
break;
case "/authors":
res.writeHead(200);
res.end(authors);
break;
default:
res.writeHead(404);
res.end(JSON.stringify({error:"Resource not found"}));
}
}ทดสอบด้วย curl http://localhost:8000/books หรือ /authors เพื่อดูข้อมูลที่เกี่ยวข้อง URL อื่น ๆ จะคืนค่าเป็นข้อผิดพลาด 404
req.url เป็นสตริงดิบที่รวมทุกอย่างหลังโดเมนไว้—เส้นทาง, สตริงคำค้น, และส่วนแยก ถ้ามีคนเข้าชม /books?id=123, case "/books" ของคุณจะไม่ตรงเพราะ req.url คือ /books?id=123 ไม่ใช่ /books พารามิเตอร์ของคำค้นทำให้เราเตอร์พัง: /books?sort=name จะไม่ตรงกับ /books ตัวอักษรที่เข้ารหัส URL ทำให้เราเตอร์พัง: /books%20reviews (ช่องว่างเข้ารหัสเป็น %20) จะไม่ตรงกับ /books reviews.
ใช้ URL API แทน: URL API จะแยก pathname ออกจาก query parameters โดยอัตโนมัติ ถอดรหัสอักขระที่เข้ารหัสใน URL และจัดการกรณีพิเศษได้อย่างถูกต้อง
const host = req.headers.host || 'localhost:8000';
const url = new URL(req.url, `http://${host}`);
switch (url.pathname) {
case '/books':
// Validate HTTP method - only GET allowed for this endpoint
if (req.method !== 'GET') {
res.writeHead(405, { 'Allow': 'GET' });
return res.end(JSON.stringify({ error: 'Method not allowed' }));
}
res.writeHead(200);
return res.end(books);
case '/authors':
if (req.method !== 'GET') {
res.writeHead(405, { 'Allow': 'GET' });
return res.end(JSON.stringify({ error: 'Method not allowed' }));
}
res.writeHead(200);
return res.end(authors);
default:
res.writeHead(404);
return res.end(JSON.stringify({ error: 'Resource not found' }));
}ทำไมต้องตรวจสอบวิธีการ HTTP? เส้นทางเดียวกันอาจรองรับวิธีการ HTTP ที่แตกต่างกัน (GET สำหรับการดึงข้อมูล, POST สำหรับการสร้างข้อมูล) หากไม่ตรวจสอบ การส่งคำขอ POST ไปยัง /books จะคืนรายการหนังสือแทนที่จะสร้างหนังสือใหม่ ซึ่งผิดหลักการ REST API รหัสสถานะ 405 Method Not Allowed พร้อมหัวข้อ Allow จะบอกไคลเอนต์ว่าวิธีการใดที่ใช้ได้สำหรับจุดสิ้นสุดนี้
การจัดการสตริงคิวรี: API URL จะทำการแยกเส้นทาง (/books) ออกจากพารามิเตอร์คิวรี (?id=123) โดยอัตโนมัติ ดังนั้น /books?id=123 จะสามารถไปยัง endpoint ของหนังสือได้ถูกต้อง ในขณะที่คุณยังสามารถเข้าถึง url.searchParams.get('id') ได้หากต้องการ
ทดสอบสถานการณ์ต่างๆ:
curl http://localhost:8000/books
curl http://localhost:8000/authors
curl http://localhost:8000/books?id=123 # Query string doesn't break routing
curl -X POST http://localhost:8000/books # Returns 405 Method Not Allowed
curl http://localhost:8000/nonexistent # Returns 404ขั้นตอนที่ 5 — การจัดการคำขอ POST และการแยกวิเคราะห์เนื้อหาของคำขอ
จนถึงตอนนี้ เราได้จัดการเฉพาะคำขอแบบ GET เท่านั้น API ส่วนใหญ่ต้องสามารถรับข้อมูลจากไคลเอนต์ ซึ่งโดยปกติจะทำผ่านคำขอแบบ POST โมดูล http ต้องมีการแยกวิเคราะห์เนื้อหาของคำขอด้วยตนเอง ซึ่งเนื้อหานั้นจะถูกส่งเข้ามาเป็นสตรีม
ข้อมูลเชิงลึกที่สำคัญ: แตกต่างจากคำขอ GET ที่ข้อมูลมาพร้อมกับ URL ร่างกายของคำขอ POST จะมาถึงเป็นกระแส ซึ่งหมายความว่าข้อมูลจะไม่มาถึงทั้งหมดในคราวเดียว แต่จะมาทีละชิ้น หากคุณพยายามแยก JSON ก่อนที่ข้อมูลทั้งหมดจะมาถึง คุณจะได้ข้อมูลที่ไม่ครบถ้วนหรือไม่ถูกต้อง คุณต้องรวบรวมข้อมูลทั้งหมดก่อน แล้วจึงค่อยแยก JSON
นี่คือสิ่งที่เกิดขึ้นถ้าคุณพยายามแยกวิเคราะห์เร็วเกินไป:
// WRONG - Body isn't complete yet
req.on('data', chunk => {
const data = JSON.parse(chunk); // Will fail - chunk is only part of the body
});
// CORRECT - Collect all chunks first
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
const data = JSON.parse(body); // Now body is complete
});สร้างเซิร์ฟเวอร์ที่รับคำขอ POST พร้อมข้อมูล JSON สร้างไฟล์ใหม่:
touch post.js
cp routes.js post.jsเปิด post.js:
nano post.jsนี่คือตัวแยกวิเคราะห์ข้อมูล JSON ที่พร้อมใช้งานในงานโปรดักชันซึ่งจัดการกับปัญหาด้านความปลอดภัยได้
const MAX_BODY = 1 * 1024 * 1024; // 1MB limit prevents memory exhaustion
function parseJSONBody(req, res) {
return new Promise((resolve, reject) => {
let body = '';
let received = 0;
// Validate Content-Type before processing
const contentType = req.headers['content-type'] || '';
if (!contentType.startsWith('application/json')) {
res.writeHead(415, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unsupported Content-Type' }));
return reject(new Error('Unsupported Content-Type'));
}
// Collect data chunks
req.on('data', chunk => {
received += chunk.length;
// Prevent memory exhaustion from huge payloads
if (received > MAX_BODY) {
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Payload too large' }));
req.destroy(); // Stop receiving more data
return reject(new Error('Payload too large'));
}
body += chunk.toString(); // Convert Buffer to string
});
// All chunks received, now parse
req.on('end', () => {
try {
if (!body) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Empty request body' }));
return reject(new Error('Empty request body'));
}
const data = JSON.parse(body);
// Validate it's an object (not a string, number, etc.)
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid JSON object' }));
return reject(new Error('Invalid JSON object'));
}
resolve(data);
} catch (parseError) {
// JSON.parse() throws for malformed JSON
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Malformed JSON' }));
console.error('JSON parse error:', parseError.message);
}
});
// Handle network errors
req.on('error', (error) => {
console.error('Request error:', error.message);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Request error' }));
});
});
}ทำไมต้องตรวจสอบ Content-Type? หากไคลเอนต์ส่งข้อมูลฟอร์มด้วย Content-Type: application/x-www-form-urlencoded การพยายามแปลงข้อมูลเป็น JSON จะล้มเหลว รหัสสถานะ 415 Unsupported Media Type จะแจ้งให้ไคลเอนต์ทราบว่า Content-Type ของพวกเขาไม่ถูกต้อง
ทำไมต้องจำกัดขนาดของร่างกาย? หากไม่มีการจำกัด ไคลเอนต์ที่ประสงค์ร้ายอาจส่งข้อมูลหลายกิกะไบต์ ทำให้หน่วยความจำของเซิร์ฟเวอร์ของคุณหมดได้ การตอบกลับ 413 Payload Too Large และ req.destroy() จะหยุดการเชื่อมต่อทันทีเมื่อเกินขีดจำกัด
ทำไมต้องตรวจสอบผลลัพธ์ที่แยกวิเคราะห์แล้ว? JSON.parse('"hello"') ประสบความสำเร็จแต่จะได้ค่าเป็นสตริง ไม่ใช่ออบเจ็กต์ โค้ดของคุณอาจคาดหวังว่า data.property จะทำงานได้ แต่สตริงไม่มีคุณสมบัติ การตรวจสอบนี้ช่วยให้แน่ใจว่าคุณได้ออบเจ็กต์จริง
ใช้ฟังก์ชันนี้ในตัวจัดการ POST ของคุณ:
const requestListener = async function (req, res) {
if (req.method === 'POST' && req.url === '/books') {
const bookData = await parseJSONBody(req, res);
// Now use bookData safely
// ...
}
};ขั้นตอนที่ 6 — การให้บริการไฟล์สแตติก
หลายแอปพลิเคชันเว็บจำเป็นต้องให้บริการไฟล์สแตติก เช่น รูปภาพ ไฟล์ CSS ไฟล์ JavaScript หรือทรัพยากรอื่น ๆ แม้ว่าโมดูล http จะไม่มีฟังก์ชันให้บริการไฟล์สแตติกในตัวเหมือน express.static ของ Express แต่คุณสามารถใช้โมดูล fs และ path ในการสร้างฟังก์ชันนี้เองได้
ประเด็นด้านความปลอดภัยที่ร้ายแรง: หากคุณใช้ path.join(__dirname, 'public', req.url) โดยไม่ตรวจสอบอย่างรอบคอบ ผู้โจมตีสามารถเข้าถึงไฟล์ใด ๆ บนเซิร์ฟเวอร์ของคุณโดยใช้การโจมตีแบบไดเร็กทอรีทราเวอร์ซัล (directory traversal attacks) การร้องขออย่าง GET /../../../etc/passwd อาจทำให้ไฟล์ระบบถูกเปิดเผย การตรวจสอบด้วย path.resolve() จะช่วยให้แน่ใจว่าเส้นทางที่แก้ไขแล้วไม่หลุดออกนอกไดเร็กทอรี public
นี่คือสิ่งที่อาจผิดพลาดได้หากไม่มีการตรวจสอบเส้นทางอย่างถูกต้อง:
// VULNERABLE - Allows directory traversal
const filePath = path.join(__dirname, 'public', req.url);
// Request: /../../../etc/passwd
// Resolves to: /etc/passwd (outside public directory!)
// SECURE - Validates path stays within public directory
const safePath = path.resolve(publicDir, '.' + reqPath);
if (!safePath.startsWith(publicDir)) {
res.writeHead(403);
res.end('Forbidden');
return;
}
// Request: /../../../etc/passwd
// safePath: /home/user/app/public/../../../etc/passwd
// Resolved: /etc/passwd
// Check fails because /etc/passwd doesn't start with /home/user/app/publicสร้างเซิร์ฟเวอร์ไฟล์คงที่ที่ปลอดภัย:
touch static.jsเปิด static.js:
nano static.jsให้บริการไฟล์สถิตอย่างมีประสิทธิภาพและปลอดภัยโดยการสตรีมด้วย fs.createReadStream() และการระบุเส้นทางอย่างปลอดภัย:
const http = require("http");
const fs = require("fs");
const path = require("path");
const host = 'localhost';
const port = 8000;
const mimeTypes = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.pdf': 'application/pdf'
};
const publicDir = path.resolve(__dirname, 'public');
const requestListener = (req, res) => {
// Normalize the request path
let reqPath = req.url === '/' ? '/index.html' : req.url;
// Strip query string - ?id=123 shouldn't affect file path
reqPath = reqPath.split('?')[0];
// Resolve path and check it stays within publicDir
// The '.' prefix prevents absolute paths from being resolved incorrectly
const safePath = path.resolve(publicDir, '.' + reqPath);
// Critical security check: ensure resolved path is within publicDir
if (!safePath.startsWith(publicDir)) {
res.writeHead(403, { 'X-Content-Type-Options': 'nosniff' });
res.end('Forbidden');
return;
}
// Check if file exists and is actually a file (not a directory)
fs.stat(safePath, (err, stat) => {
if (err || !stat.isFile()) {
res.writeHead(404, { 'X-Content-Type-Options': 'nosniff' });
res.end('File not found');
return;
}
// Determine MIME type from file extension
const ext = path.extname(safePath).toLowerCase();
const contentType = mimeTypes[ext] || 'application/octet-stream';
res.setHeader("Content-Type", contentType);
res.setHeader("X-Content-Type-Options", "nosniff"); // Prevents MIME sniffing attacks
res.writeHead(200);
// Stream file instead of loading into memory
// Critical for large files - won't consume all memory
const stream = fs.createReadStream(safePath);
stream.pipe(res);
stream.on('error', () => {
// File might be deleted between stat() and createReadStream()
res.writeHead(500, { 'X-Content-Type-Options': 'nosniff' });
res.end('Server error');
});
});
};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
console.log(`Serving static files from: ${publicDir}`);
});สร้างไดเรกทอรี public และเพิ่มไฟล์ทดสอบ:
mkdir public
echo "<h1>Static File Served!</h1>" > public/index.htmlรันเซิร์ฟเวอร์:
node static.jsเยี่ยมชม http://localhost:8000 ในเบราว์เซอร์ของคุณเพื่อดูไฟล์สแตติกที่ให้บริการ
ทำไมต้องใช้การสตรีมแทน fs.readFile()? สำหรับไฟล์วิดีโอขนาด 100MB, fs.readFile() จะโหลดทั้งไฟล์เข้าสู่หน่วยความจำก่อนที่จะส่งไป ในขณะที่ fs.createReadStream() ข้อมูลจะไหลตรงจากดิสก์ไปยังเครือข่ายเป็นชิ้นเล็ก ๆ ใช้หน่วยความจำน้อยที่สุดไม่ว่าจะเป็นขนาดไฟล์เท่าใด
ทำไมต้องตรวจสอบ stat.isFile()? หากมีคนขอเส้นทางโฟลเดอร์ เช่น /images/ fs.stat() จะสำเร็จ แต่ isFile() จะคืนค่า false หากไม่มีการตรวจสอบนี้ คุณอาจพยายามสตรีมโฟลเดอร์ ซึ่งจะล้มเหลว การตรวจสอบนี้ช่วยให้มั่นใจว่าคุณให้บริการเฉพาะไฟล์จริงเท่านั้น
ทดสอบการป้องกันการเข้าถึงไฟล์แบบเส้นทางผิด
curl http://localhost:8000/../../../etc/passwd
# Should return 403 Forbidden, not the system fileขั้นตอนที่ 7 — การจัดการข้อผิดพลาดและการบันทึก
การจัดการข้อผิดพลาดอย่างเข้มงวดเป็นสิ่งสำคัญสำหรับเซิร์ฟเวอร์ที่ใช้งานจริง โมดูล http ไม่ได้ให้การจัดการข้อผิดพลาดโดยอัตโนมัติ ดังนั้นคุณจึงต้องดำเนินการเอง
การจัดการข้อผิดพลาดอย่างถูกต้องเกี่ยวข้องกับการจับข้อผิดพลาดแบบอะซิงโครนัส บันทึกข้อผิดพลาดอย่างเหมาะสม ส่งการตอบกลับข้อผิดพลาดที่เป็นมิตรต่อผู้ใช้ และป้องกันไม่ให้เซิร์ฟเวอร์ล่มจากข้อผิดพลาดที่ไม่ได้จัดการ มาปรับปรุงเซิร์ฟเวอร์ของเราด้วยการจัดการข้อผิดพลาดและการบันทึกอย่างครอบคลุมกันเถอะ:
const http = require("http");
const fs = require('fs').promises;
const host = 'localhost';
const port = 8000;
// Simple logging utility
const log = {
info: (msg) => console.log(`[INFO] ${new Date().toISOString()} - ${msg}`),
error: (msg, err) => console.error(`[ERROR] ${new Date().toISOString()} - ${msg}`, err),
warn: (msg) => console.warn(`[WARN] ${new Date().toISOString()} - ${msg}`)
};
const requestListener = async function (req, res) {
try {
log.info(`${req.method} ${req.url} - ${req.headers['user-agent'] || 'Unknown'}`);
if (req.url === '/') {
try {
const data = await fs.readFile(__dirname + '/index.html');
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(data);
} catch (error) {
log.error('Failed to read index.html', error);
res.writeHead(500);
res.end('<h1>Internal Server Error</h1><p>Unable to load page.</p>');
}
} else if (req.url === '/error') {
throw new Error('Simulated error for testing');
} else {
res.writeHead(404);
res.end('<h1>Not Found</h1><p>The requested resource was not found.</p>');
}
} catch (error) {
log.error('Unhandled error in request handler', error);
const errorMessage = process.env.NODE_ENV !== 'production'
? error.message
: 'An unexpected error occurred';
res.writeHead(500);
res.end(`<h1>Internal Server Error</h1><p>${errorMessage}</p>`);
}
};
const server = http.createServer(requestListener);
// Handle server-level errors
server.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
log.error(`Port ${port} is already in use`);
} else {
log.error('Server error', error);
}
process.exit(1);
});
// Handle client connection errors
server.on('clientError', (err, socket) => {
log.warn('Client error: ' + err.message);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(port, host, () => {
log.info(`Server is running on http://${host}:${port}`);
});การปิดระบบอย่างสง่างาม (SIGTERM/SIGINT)
เมื่อรันภายใต้ตัวจัดการกระบวนการอย่าง PM2 หรือ Docker ให้จัดการสัญญาณการยุติเพื่อปิดเซิร์ฟเวอร์อย่างเรียบร้อย:
const connections = new Set();
server.on('connection', socket => {
connections.add(socket);
socket.on('close', () => connections.delete(socket));
});
const shutdown = (signal) => {
log.warn(`${signal} received: shutting down gracefully...`);
server.close(() => {
log.info('Server closed.');
process.exit(0);
});
setTimeout(() => {
log.warn('Forcing socket close...');
connections.forEach(s => s.destroy());
process.exit(1);
}, 10000).unref();
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));การปฏิบัติที่สำคัญในการจัดการข้อผิดพลาดแสดงดังนี้:
- บล็อก try-catch รอบๆ การทำงานแบบอะซิงก์
- ตัวจัดการข้อผิดพลาดระดับเซิร์ฟเวอร์ สำหรับปัญหาการเชื่อมต่อและพอร์ต
- การจัดการข้อผิดพลาดของลูกค้า สำหรับคำขอที่ไม่ถูกต้อง
- การบันทึก พร้อมวันที่และระดับความรุนแรง
- ข้อความแสดงข้อผิดพลาดที่ตระหนักถึงสภาพแวดล้อม (อย่าเปิดเผย stack trace ในการผลิต)
- การตอบกลับข้อผิดพลาดอย่างมีความสุภาพ พร้อมรหัสสถานะ HTTP ที่เหมาะสม
แนวทางปฏิบัติที่ดีที่สุด: ในการใช้งานจริง ควรพิจารณาใช้ไลบรารีการบันทึกแบบมีโครงสร้าง เช่น Winston หรือ Pino และส่งข้อผิดพลาดสำคัญไปยังบริการติดตามข้อผิดพลาด เช่น Sentry หรือ Rollbar ตัวอย่างนี้ใช้การบันทึกผ่านคอนโซลแบบง่ายเพื่อการศึกษา
ขั้นตอนที่ 8 — การใช้ไวยากรณ์โมดูล ES (ESM)
Node.js รุ่นใหม่รองรับ ES Modules (ESM) โดยใช้ไวยากรณ์ import/export แทน CommonJS require/module.exports ในการใช้ ESM คุณสามารถตั้งชื่อไฟล์ด้วยนามสกุล .mjs หรือกำหนด "type": "module" ในไฟล์ package.json ของคุณ
ไวยากรณ์ ESM ใช้ import แทน require และ export แทน module.exports นี่คือตัวอย่างเซิร์ฟเวอร์พื้นฐานเดียวกันโดยใช้ไวยากรณ์โมดูล ES:
สร้าง server.mjs:
touch server.mjs
nano server.mjsimport http from 'http';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
// ES Modules don't have __dirname, so we recreate it
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const host = 'localhost';
const port = 8000;
const requestListener = async (req, res) => {
try {
if (req.url === '/') {
const data = await fs.readFile(path.join(__dirname, 'index.html'));
res.setHeader("Content-Type", "text/html");
res.writeHead(200);
res.end(data);
} else {
res.writeHead(404);
res.end('Not found');
}
} catch (error) {
console.error('Error:', error);
res.writeHead(500);
res.end('Server error');
}
};
const server = http.createServer(requestListener);
server.listen(port, host, () => {
console.log(`Server is running on http://${host}:${port}`);
});รันมัน:
node server.mjsความแตกต่างหลัก: ใน ESM, __dirname จะไม่สามารถใช้ได้—you ต้องสร้างมันจาก import.meta.url นอกจากนี้ เส้นทางไฟล์ในการใช้งาน fs อาจจำเป็นต้องระบุการแก้ไขอย่างชัดเจน ไม่อย่างนั้น API ของโมดูล HTTP จะเหมือนเดิม
อีกทางหนึ่ง ให้สร้าง package.json โดยมี "type": "module":
{
"type": "module"
}จากนั้นใช้ไฟล์ .js พร้อมกับไวยากรณ์ ESM:
import http from 'http';
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('ESM Server');
});
server.listen(8000);การพิจารณาด้านการผลิต
เมื่อก้าวจากการพัฒนาไปสู่การใช้งานจริง มีหลายปัจจัยสำคัญที่ต้องพิจารณา:
รองรับ HTTPS
โมดูล http ใช้สำหรับ HTTP เท่านั้น สำหรับ HTTPS ให้ใช้โมดูล https ในลักษณะเดียวกัน:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('path/to/private-key.pem'),
cert: fs.readFileSync('path/to/certificate.pem')
};
const server = https.createServer(options, requestListener);ความปลอดภัย: อย่าเก็บคีย์ส่วนตัวหรือใบรับรองในระบบควบคุมเวอร์ชัน ใช้ตัวแปรสภาพแวดล้อมหรือบริการจัดการความลับที่ปลอดภัยในสภาพแวดล้อมการผลิต
การกำหนดค่ารีเวิร์สพร็อกซี
ในการใช้งานจริง เซิร์ฟเวอร์ Node.js มักทำงานอยู่หลังพร็อกซีย้อนกลับ เช่น Nginx หรือ Apache พร็อกซีจะดูแลการยกเลิกการเข้ารหัส SSL ไฟล์สแตติก และการกระจายโหลด เซิร์ฟเวอร์ Node.js ของคุณควร:
- ฟังเฉพาะที่
localhostหรือ127.0.0.1เท่านั้น - อ่านเฮดเดอร์
X-Forwarded-Forสำหรับ IP ของลูกค้า - ไว้วางใจเฮดเดอร์พร็อกซีเฉพาะจากเซิร์ฟเวอร์พร็อกซีของคุณเท่านั้น
ดูคู่มือของเราเกี่ยวกับวิธีการตั้งค่า Nginx เป็น Reverse Proxy บน Ubuntu สำหรับรายละเอียดการตั้งค่า
การจัดการกระบวนการ
สำหรับการผลิต ใช้ตัวจัดการกระบวนการ เช่น PM2 เพื่อจัดการ:
- รีสตาร์ทอัตโนมัติเมื่อเกิดข้อผิดพลาด
- การปรับใช้งานโดยไม่มีเวลาหยุดทำงาน
- การจัดการบันทึก
- โหมดคลัสเตอร์สำหรับการใช้หลายคอร์ CPU
เรียนรู้เพิ่มเติมได้ในคู่มือของเราเกี่ยวกับวิธีการตั้งค่าแอปพลิเคชัน Node.js สำหรับการใช้งานจริงบน Ubuntu
การเพิ่มประสิทธิภาพ
การแคชไฟล์: โหลดไฟล์สเตติกตอนเริ่มต้น (ตามที่แสดงในขั้นตอนที่ 3) แทนที่จะอ่านจากดิสก์ทุกครั้งที่มีคำร้องขอ ซึ่งช่วยลดภาระ I/O ได้อย่างมาก
สตรีมไฟล์ขนาดใหญ่: สำหรับไฟล์ที่มีขนาดเกิน 1MB ใช้
fs.createReadStream()และต่อไปยังการตอบกลับแทนการโหลดไฟล์ทั้งหมดเข้าสู่หน่วยความจำการเชื่อมต่อแบบพูล: สำหรับการเชื่อมต่อฐานข้อมูล ใช้การเชื่อมต่อแบบพูลเพื่อลดภาระจากการสร้างการเชื่อมต่อใหม่ในแต่ละคำขอ
การบีบอัด: ใช้โมดูล
zlibสำหรับการบีบอัด gzip บนข้อความเพื่อลดปริมาณแบนด์วิดท์:const zlib = require('zlib'); const gzip = zlib.createGzip(); res.setHeader('Content-Encoding', 'gzip'); gzip.pipe(res); gzip.write(data); gzip.end();โหมดคลัสเตอร์: ใช้โมดูล
clusterของ Node.js เพื่อสร้างโปรเซสเวิร์กเกอร์ให้เท่ากับจำนวนคอร์ CPU ทำให้เพิ่มประสิทธิภาพสูงสุดสำหรับงานที่ต้องใช้ CPU มากหมดเวลาการร้องขอ: ตั้งค่าหมดเวลาอย่างเหมาะสมเพื่อป้องกันไม่ให้ลูกค้าช้าที่ใช้ทรัพยากรมากเกินไป:
server.setTimeout(30000); // 30 secondsการตรวจสอบหน่วยความจำ: ติดตามการใช้งานหน่วยความจำเพื่อตรวจจับการรั่วไหลตั้งแต่เนิ่นๆ ในการใช้งานจริง ควรติดตามขนาด heap และทำการรีสตาร์ทอย่างนุ่มนวลหากหน่วยความจำเติบโตเกินขอบเขต
แนวปฏิบัติที่ดีในการแคชและบีบอัด
เพื่อปรับปรุงประสิทธิภาพและลดเวลาในการตอบสนอง ควรใช้การเก็บแคชและการบีบอัดเมื่อเหมาะสม
เปิดใช้งานการแคชสำหรับไฟล์คงที่:
เพิ่มหัวข้อCache-ControlและETagในการตอบกลับไฟล์คงที่ของคุณ ตัวอย่างเช่น:const stat = fs.statSync(safePath); res.setHeader('Cache-Control', 'public, max-age=86400'); // cache for 1 day res.setHeader('ETag', `"${stat.size}-${stat.mtime.getTime()}"`);เบราว์เซอร์และ CDN จะใช้สำเนาที่แคชไว้ซ้ำจนกว่า ETag หรือระยะเวลาการแคชจะหมดอายุ
ใช้การบีบอัด gzip หรือ Brotli:
การตอบกลับที่เป็นข้อความ เช่น HTML, CSS, JS และ JSON สามารถบีบอัดได้ดีมาก คุณสามารถใช้การบีบอัดด้วยโมดูลzlibของ Node:const zlib = require('zlib'); const stream = fs.createReadStream(safePath); res.setHeader('Content-Encoding', 'gzip'); stream.pipe(zlib.createGzip()).pipe(res);สิ่งนี้ช่วยลดการใช้แบนด์วิดท์และเร่งการส่งข้อมูลสำหรับลูกค้าที่มีการเชื่อมต่อช้าลง
หลีกเลี่ยงการจัดเก็บแคชของจุดเชื่อมต่อที่เปลี่ยนแปลงบ่อย:
สำหรับ API หรือข้อมูลที่อัปเดตบ่อย ควรกำหนดระยะเวลาการเก็บแคชให้สั้นลง (เช่นmax-age=60) หรือใช้no-storeเมื่อจำเป็น
การปรับแต่งเหล่านี้ช่วยเสริมการสตรีมไฟล์แบบสแตติกและการปิดระบบอย่างเรียบร้อย ทำให้เซิร์ฟเวอร์ Node.js ของคุณมอบประสิทธิภาพสูงในขณะเดียวกันก็ยังคงปลอดภัยและคาดการณ์ได้
ใช้โมดูล http ของ Node.js สำหรับ API ขั้นพื้นฐาน การกำหนดเส้นทางแบบกำหนดเอง หรือการเรียนรู้รายละเอียดภายในของ HTTP.
ใช้ Express/Fastify เมื่อคุณต้องการ middleware, การสร้างเทมเพลต หรือการทำต้นแบบอย่างรวดเร็ว.
การแก้ไขปัญหาที่พบบ่อย
พอร์ตถูกใช้งานแล้ว (EADDRINUSE)
ข้อผิดพลาด: ข้อผิดพลาด: listen EADDRINUSE: ที่อยู่นี้ถูกใช้งานแล้ว :::8000
วิธีแก้ปัญหา:
- ค้นหากระบวนการที่ใช้พอร์ต:
lsof -i :8000(macOS/Linux) หรือnetstat -ano | findstr :8000(Windows) - ฆ่ากระบวนการนี้หรือใช้พอร์ตอื่น
- เพิ่มการจัดการข้อผิดพลาด (ตามที่แสดงในขั้นตอนที่ 7) เพื่อจัดการกับสิ่งนี้อย่างราบรื่น
ไม่พบข้อมูลคำขอ
ปัญหา: ตัวเนื้อหาของคำขอ POST ว่างหรือไม่ถูกกำหนด
วิธีแก้ปัญหา: ร่างกายของคำขอเป็นสตรีม—คุณต้องรวบรวมชิ้นส่วนเสมอ ใช้รูปแบบที่แสดงในขั้นตอนที่ 5 เสมอ:
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
// Now parse body
});ข้อผิดพลาด CORS ในเบราว์เซอร์
ปัญหา: เบราว์เซอร์บล็อกคำขอเนื่องจากนโยบาย CORS
วิธีแก้ไข: เพิ่มหัวข้อ CORS ในการตอบกลับของคุณ:
res.setHeader("Access-Control-Allow-Origin", "*"); // Or specific domain
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");ความปลอดภัย: การใช้ "*" สำหรับ Access-Control-Allow-Origin จะอนุญาตทุกแหล่งที่มา ในการใช้งานจริง ควรกำหนดโดเมนของหน้าเว็บของคุณให้ชัดเจน
ไฟล์คงที่ไม่พบ (404)
ปัญหา: ไม่พบไฟล์สแตติกแม้ว่าจะมีอยู่
วิธีแก้ปัญหา:
- ตรวจสอบการแก้ไขเส้นทางไฟล์
- ตรวจสอบว่า
__dirnameชี้ไปยังไดเรกทอรีที่ถูกต้อง - ตรวจสอบให้แน่ใจว่าสิทธิ์ของไฟล์อนุญาตให้สามารถอ่านได้
- ตรวจสอบให้แน่ใจว่าการล้างเส้นทางไม่ลบตัวอักษรที่ถูกต้อง
คำถามที่พบบ่อย
โมดูล HTTP ใน Node.js คืออะไร?
โมดูล http เป็นโมดูลในตัวของ Node.js สำหรับสร้างเซิร์ฟเวอร์และไคลเอนต์ HTTP มันให้ API ระดับต่ำสำหรับจัดการคำขอและการตอบสนอง HTTP โดยไม่ต้องพึ่งพาโมดูลภายนอก มันเป็นส่วนหนึ่งของแกนหลักของ Node.js ดังนั้นจึงมีให้ใช้งานในทุกการติดตั้ง Node.js
ฉันจะสร้างเว็บเซิร์ฟเวอร์ง่ายๆ ใน Node.js ได้อย่างไร?
สร้างเซิร์ฟเวอร์โดยใช้ http.createServer() พร้อมฟังก์ชันตัวฟังคำขอ:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
});
server.listen(8000);สิ่งนี้สร้างเซิร์ฟเวอร์ที่คอยฟังบนพอร์ต 8000 ซึ่งตอบกลับทุกคำขอด้วย "Hello World".
ความแตกต่างระหว่างโมดูล HTTP กับ Express.js คืออะไร?
โมดูล http เป็นฟังก์ชันหลักของ Node.js ที่ให้การจัดการ HTTP ในระดับต่ำ Express.js เป็นเฟรมเวิร์กที่สร้างขึ้นบน http ซึ่งเพิ่มฟังก์ชันการทำงานเช่น routing, middleware, templating และความสะดวกสบายอื่น ๆ โมดูล http ให้คุณควบคุมได้อย่างเต็มที่ แต่ต้องตั้งค่าด้วยตนเองมากขึ้น ในขณะที่ Express.js ให้การใช้ abstraction แลกกับการมี dependency เพิ่มเติม
ใช้โมดูล http เมื่อ:
- การสร้าง API หรือไมโครเซอร์วิสที่น้ำหนักเบา
- การเรียนรู้พื้นฐานของ HTTP
- คุณต้องการไม่มีการพึ่งพาใด ๆ
- ควบคุมการจัดการคำขอ/การตอบสนองได้อย่างเต็มที่
ใช้ Express.js เมื่อ:
- การสร้างแอปพลิเคชันที่ซับซ้อนโดยมีหลายเส้นทาง
- คุณต้องการระบบนิเวศของมิดเดิลแวร์
- การสร้างต้นแบบอย่างรวดเร็วเป็นสิ่งที่ต้องให้ความสำคัญ
- คุณต้องการรูปแบบมาตรฐานของชุมชน
สำหรับการแนะนำอย่างครบถ้วนเกี่ยวกับ Express.js และวิธีที่มันช่วยให้ง่ายต่อการพัฒนาเว็บ ดูคู่มือของเราเกี่ยวกับพื้นฐาน Node.js Express
ฉันสามารถสร้าง REST API ด้วย Node.js โดยไม่ใช้ Express ได้ไหม?
ใช่ แน่นอน โมดูล http นั้นเพียงพอสำหรับการสร้าง REST API คุณจะต้องจัดการด้วยตัวเอง:
- การแยกเส้นทาง (ใช้
req.urlและreq.method) - การแยกวิเคราะห์เนื้อหาคำขอ (เก็บชิ้นส่วนสตรีม)
- รูปแบบการตอบกลับ
- การจัดการข้อผิดพลาด
สำหรับ API ที่เรียบง่าย วิธีนี้มักจะดีกว่าการเพิ่มการพึ่งพาของเฟรมเวิร์ก ดูขั้นตอนที่ 4 และ 5 ในบทช่วยสอนนี้สำหรับตัวอย่างการจัดการ routing และคำขอ POST
ฉันควรจัดการเส้นทางที่แตกต่างกันโดยใช้โมดูล HTTP ของ Node.js อย่างไร?
แยกวิเคราะห์ req.url และใช้ตรรกะเงื่อนไขหรือคำสั่งสวิตช์:
const requestListener = (req, res) => {
switch (req.url) {
case '/':
res.writeHead(200);
res.end('Home page');
break;
case '/about':
res.writeHead(200);
res.end('About page');
break;
default:
res.writeHead(404);
res.end('Not found');
}
};สำหรับการกำหนดเส้นทางที่ซับซ้อนมากขึ้น คุณสามารถวิเคราะห์เส้นทาง URL และสตริงการค้นหาโดยใช้โมดูล url และจัดการกับ HTTP วิธีการต่างๆ ได้
ฉันจะส่งการตอบสนองเป็น HTML หรือ JSON ด้วยโมดูล HTTP ได้อย่างไร?
ตั้งค่าเฮดเดอร์ Content-Type ให้เหมาะสมแล้วส่งข้อมูล:
HTML:
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end('<h1>Hello</h1>');เจสัน:
res.setHeader('Content-Type', 'application/json');
res.writeHead(200);
res.end(JSON.stringify({ message: 'Hello' }));หัวข้อ Content-Type บอกให้ไคลเอนต์ทราบว่าจะตีความข้อมูลการตอบสนองอย่างไร
ฉันควรใช้พอร์ตอะไรสำหรับเซิร์ฟเวอร์ Node.js ในเครื่อง?
พอร์ตสำหรับการพัฒนาที่พบบ่อยมีดังนี้:
- 8000 หรือ 8080: การพัฒนา HTTP ทั่วไป
- 3000: เป็นที่นิยมกับเฟรมเวิร์กอย่าง Express
- 5000: ก็เป็นเรื่องธรรมดาเช่นกัน
พอร์ตใด ๆ ที่สูงกว่า 1024 ใช้งานได้สำหรับผู้ใช้ที่ไม่ใช่ผู้ดูแลระบบ พอร์ตที่ต่ำกว่า 1024 ต้องการสิทธิ์ผู้ดูแลระบบ ตรวจสอบความพร้อมใช้งานด้วย lsof -i :PORT (macOS/Linux) ก่อนใช้งาน
ฉันจะจัดการข้อผิดพลาด 404 ในเซิร์ฟเวอร์ HTTP ของ Node.js ได้อย่างไร?
เพิ่มกรณีเริ่มต้นในตรรกะการกำหนดเส้นทางของคุณ:
const requestListener = (req, res) => {
if (req.url === '/') {
res.writeHead(200);
res.end('Home');
} else {
res.writeHead(404);
res.end('Not Found');
}
};ควรกำหนดรหัสสถานะเป็น 404 สำหรับเส้นทางที่ไม่มีอยู่จริงเพื่อให้เป็นไปตามมาตรฐาน HTTP
ฉันจะให้บริการไฟล์คงที่โดยไม่ใช้ Express ได้อย่างไร?
ใช้โมดูล fs เพื่ออ่านไฟล์ และโมดูล path สำหรับการแก้ไขเส้นทางอย่างปลอดภัย:
const fs = require('fs').promises;
const path = require('path');
const requestListener = async (req, res) => {
const filePath = path.join(__dirname, 'public', req.url);
try {
const data = await fs.readFile(filePath);
res.writeHead(200);
res.end(data);
} catch (error) {
res.writeHead(404);
res.end('File not found');
}
};ตรวจสอบเส้นทางอยู่เสมอเพื่อป้องกันการโจมตีแบบเข้าถึงไดเรกทอรี (ดูขั้นตอนที่ 6 สำหรับวิธีการที่ปลอดภัย)
ฉันสามารถใช้ ES Modules (import) กับโมดูล HTTP ใน Node.js ได้ไหม?
ใช่, Node.js รองรับ ES Modules ใช้ได้ทั้ง:
- ไฟล์ที่มีนามสกุล
.mjs:
import http from 'http';
// ... rest of code- ตั้งค่า
"type": "module"ในpackage.jsonจากนั้นใช้ไฟล์.js:
{
"type": "module"
}โปรดทราบว่า ES Modules ไม่มี __dirname — ให้สร้างจาก import.meta.url ตามที่แสดงในขั้นตอนที่ 8
ฉันจะแยกวิเคราะห์เนื้อหาของคำขอ POST ด้วยโมดูล HTTP ได้อย่างไร?
ข้อมูลใน POST จะมาถึงเป็นสตรีม รวบรวมชิ้นส่วนแล้วค่อยแยกวิเคราะห์:
const parseBody = (req) => {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', chunk => body += chunk.toString());
req.on('end', () => {
try {
resolve(JSON.parse(body)); // For JSON
} catch (error) {
reject(error);
}
});
req.on('error', reject);
});
};รูปแบบนี้สามารถใช้ได้กับ JSON, ข้อมูลฟอร์ม หรือประเภทเนื้อหาที่เป็นข้อความทุกประเภท
โมดูล HTTP เหมาะสำหรับใช้งานในงานจริงหรือไม่?
ใช่ โมดูล http พร้อมใช้งานในสภาพแวดล้อมการผลิตและถูกใช้งานโดยแอปพลิเคชัน Node.js หลายตัว อย่างไรก็ตาม ให้พิจารณา:
- การจัดการกระบวนการ: ใช้ PM2 หรือโปรแกรมที่คล้ายกันสำหรับการรีสตาร์ทอัตโนมัติ
- พร็อกซีแบบย้อนกลับ: ทำงานหลัง Nginx/Apache สำหรับ SSL และบาลานซ์โหลด
- การจัดการข้อผิดพลาด: ใช้การจัดการข้อผิดพลาดและการบันทึกอย่างครอบคลุม
- ความปลอดภัย: ตรวจสอบข้อมูลนำเข้า ทำความสะอาดเส้นทาง กำหนดขนาดสูงสุดของคำขอ
- ประสิทธิภาพ: แคชไฟล์สแตติก ใช้การกระจายโหลดสำหรับงานที่ใช้ CPU สูง
แอปพลิเคชันในการผลิตหลายตัวใช้โมดูล http โดยตรง โดยเฉพาะไมโครเซอร์วิสและ API ขนาดเล็ก สำหรับคำแนะนำการปรับใช้ในสภาพแวดล้อมการผลิตอย่างครบถ้วน ดูวิธีการตั้งค่าแอปพลิเคชัน Node.js สำหรับการผลิตบน Ubuntu 22.04
บทสรุป
ในบทแนะนำนี้ คุณได้สร้างเซิร์ฟเวอร์ HTTP ด้วย Node.js ที่พร้อมใช้งานจริงตั้งแต่เริ่มต้น คุณเริ่มจากการตอบกลับข้อความแบบพื้นฐาน แล้วต่อยอดไปสู่การให้บริการเนื้อหาประเภทต่าง ๆ (JSON, CSV และ HTML) คุณได้ทำการติดตั้งระบบ routing เพื่อจัดการ URL ที่ต่างกัน วิเคราะห์เนื้อหาของคำขอ POST ให้บริการไฟล์ static อย่างปลอดภัย และเพิ่มการจัดการข้อผิดพลาดและการบันทึกอย่างครบถ้วน นอกจากนี้คุณยังได้สำรวจทั้งซินแท็กซ์ CommonJS และ ES Module พร้อมกับพิจารณาการปรับใช้ในสภาพแวดล้อมการผลิต
ตอนนี้คุณมีความพร้อมในการสร้างเว็บเซิร์ฟเวอร์ที่ตอบสนองความต้องการที่หลากหลายได้โดยไม่ต้องพึ่งพาเฟรมเวิร์ก ด้วยความรู้นี้ คุณสามารถ:
- สร้าง REST API น้ำหนักเบาสำหรับไมโครเซอร์วิส
- ให้บริการเว็บไซต์หลายหน้าด้วยการจัดการเส้นทางอย่างเหมาะสม
- จัดการการอัปโหลดและดาวน์โหลดไฟล์
- นำตรรกะของมิดเดิลแวร์ที่กำหนดเองไปใช้
- ตัดสินใจอย่างมีข้อมูลเกี่ยวกับเวลาที่เฟรมเวิร์กเพิ่มคุณค่าเทียบกับความซับซ้อนที่ไม่จำเป็น
ขั้นตอนถัดไป:
- เจาะลึกพื้นฐานของ Node.js: กลับไปที่ซีรีส์ How To Code in Node.js เพื่อการเรียนรู้อย่างครบถ้วน
- สำรวจเอกสารอย่างเป็นทางการ: เอกสารของโมดูล HTTP ของ Node.js ประกอบด้วยเอกสารอ้างอิง API อย่างละเอียดและตัวอย่างการใช้งานขั้นสูง
- เมื่อคุณต้องการฟีเจอร์ของเฟรมเวิร์ก: พิจารณาพื้นฐานของ Express.js สำหรับการพัฒนาที่รวดเร็วพร้อมการสนับสนุนมิดเดิลแวร์และเทมเพลต
การเข้าใจพื้นฐานของ HTTP เหล่านี้ทำให้คุณเป็นนักพัฒนา Node.js ที่มีความสามารถมากขึ้น ไม่ว่าคุณจะเลือกใช้เฟรมเวิร์กหรือทำงานโดยตรงกับโมดูล http คุณก็จะเข้าใจกลไกพื้นฐานที่ขับเคลื่อนแอปพลิเคชันเว็บ Node.js ทั้งหมด ความรู้นี้ช่วยให้คุณแก้ไขปัญหา ปรับปรุงประสิทธิภาพ และออกแบบสถาปัตยกรรมระบบที่สามารถขยายได้อย่างมีประสิทธิภาพ
ขอบคุณที่เรียนรู้ร่วมกับชุมชน Linux-Console.net

