สถานะของหนังสือเล่มนี้
สิ่งที่คุณกำลังอ่านเป็นรุ่นสุดท้ายของหนังสือ ดังนั้นการอัปเดตจะเกิดขึ้นเมื่อมีการแก้ไขข้อผิดพลาดและการแก้ไขที่สอดคล้องกันกับ Node.Js เวอร์ชันใหม่
กรณีรหัสในหนังสือเล่มนี้ได้รับการทดสอบใน node.js เวอร์ชัน 0.6.11 และสามารถทำงานได้อย่างถูกต้อง
วัตถุผู้อ่าน
หนังสือเล่มนี้ดีที่สุดสำหรับผู้อ่านที่มีภูมิหลังทางเทคนิคคล้ายกับฉัน: อย่างน้อยก็มีประสบการณ์ในภาษาที่มุ่งเน้นวัตถุเช่น Ruby, Python, PHP หรือ Java; JS.
นี่หมายถึงนักพัฒนาที่มีประสบการณ์ในภาษาการเขียนโปรแกรมอื่น ๆ เพื่อให้เข้าใจหนังสือเล่มนี้ฉันคิดว่าคุณรู้แนวคิดพื้นฐานเหล่านี้แล้ว
อย่างไรก็ตามหนังสือเล่มนี้จะแนะนำฟังก์ชั่นและวัตถุใน JavaScript ในรายละเอียดเนื่องจากพวกเขาแตกต่างจากฟังก์ชั่นและวัตถุในภาษาการเขียนโปรแกรมอื่น ๆ ที่คล้ายกันมาก
โครงสร้างของหนังสือเล่มนี้
หลังจากอ่านหนังสือเล่มนี้คุณจะกรอกเว็บแอปพลิเคชันที่สมบูรณ์ซึ่งช่วยให้ผู้ใช้สามารถเรียกดูหน้าและอัปโหลดไฟล์ได้
แน่นอนว่าแอปพลิเคชันนั้นไม่มีอะไรดีเมื่อเทียบกับรหัสที่เขียนขึ้นเพื่อใช้งานฟังก์ชั่นนี้เรามีความกังวลมากขึ้นเกี่ยวกับวิธีการสร้างกรอบการทำงานเพื่อตัดโมดูลที่แตกต่างกันของแอปพลิเคชันของเรา มันไม่ลึกลับมากเหรอ? คุณจะเข้าใจในภายหลัง
หนังสือเล่มนี้เริ่มต้นด้วยการแนะนำความแตกต่างระหว่างการพัฒนา JavaScript ในสภาพแวดล้อม Node.js และการพัฒนา JavaScript ในสภาพแวดล้อมของเบราว์เซอร์
ทันทีหลังจากนั้นเราจะนำทุกคนไปสู่แอปพลิเคชัน "Hello World" แบบดั้งเดิมที่สุดซึ่งเป็นแอปพลิเคชัน Node.js ขั้นพื้นฐานที่สุด
ในที่สุดฉันจะหารือกับคุณถึงวิธีการออกแบบแอปพลิเคชันที่สมบูรณ์ "จริง" วิเคราะห์โมดูลที่แตกต่างกันที่จำเป็นต้องใช้เพื่อเติมแอปพลิเคชันให้สมบูรณ์และแนะนำวิธีการใช้โมดูลเหล่านี้ทีละขั้นตอน
สิ่งที่รับประกันได้คือในกระบวนการนี้คุณจะได้เรียนรู้แนวคิดขั้นสูงใน JavaScript วิธีการใช้และทำไมแนวคิดเหล่านี้สามารถนำไปใช้ในขณะที่แนวคิดที่คล้ายกันในภาษาการเขียนโปรแกรมอื่น ๆ ไม่สามารถนำไปใช้ได้
รหัสแหล่งที่มาทั้งหมดของแอปพลิเคชันนี้สามารถเข้าถึงได้ผ่านที่เก็บรหัส GitHub ในหนังสือเล่มนี้: https://github.com/manuelkiessling/nodeBeginnerbook/tree/master/code/Application
javascript และ node.js
JavaScript และคุณ
การวางเทคโนโลยีกันมาพูดคุยเกี่ยวกับคุณและความสัมพันธ์ของคุณกับ JavaScript ก่อน จุดประสงค์หลักของบทนี้คือเพื่อให้คุณดูว่าจำเป็นสำหรับคุณที่จะอ่านเนื้อหาของบทที่ตามมาต่อไปหรือไม่
หากคุณเป็นเหมือนฉันคุณก็เริ่ม "พัฒนา" ด้วย HTML เร็วมาก
สิ่งที่คุณต้องการคือ "สิ่งแห้ง" คุณต้องการทราบวิธีการสร้างเว็บไซต์ที่ซับซ้อน - ดังนั้นคุณจึงเรียนรู้ภาษาการเขียนโปรแกรมเช่น PHP, Ruby, Java และเริ่มเขียนรหัส "Back -End"
ในเวลาเดียวกันคุณมักจะให้ความสนใจกับ JavaScript เปิด () มันง่ายมาก -
อย่างไรก็ตามสิ่งเหล่านี้เป็นเทคโนโลยีส่วนหน้าทั้งหมดหลังจากทั้งหมด
จากนั้น node.js, JavaScript บนเซิร์ฟเวอร์มันเจ๋งแค่ไหน?
ดังนั้นคุณรู้สึกว่าถึงเวลาที่จะได้รับทั้ง JavaScript ที่คุ้นเคยและไม่คุ้นเคยอีกครั้ง แต่ไม่ต้องกังวลการเขียนแอปพลิเคชัน Node.js เป็นสิ่งที่เข้าใจได้ ครั้งนี้ฉันเล่นมันจริง
ปัญหามาที่นี่: เนื่องจากจาวาสคริปต์มีอยู่จริงในสองหรือสามรูปแบบ (จากของเล่นขนาดเล็กที่เพิ่ม DHTML ในปี 1990 ไปจนถึงเทคโนโลยีส่วนหน้าในแง่ที่เข้มงวดเช่น jQuery และจนถึงตอนนี้) จึงยากที่จะหา วิธี "ถูกต้อง" ในการเรียนรู้ JavaScript เพื่อให้คุณรู้สึกว่าคุณกำลังพัฒนามันมากกว่าที่จะใช้เมื่อเขียนแอปพลิเคชัน Node.js
เพราะนั่นคือกุญแจสำคัญ: คุณเป็นนักพัฒนาที่มีประสบการณ์อยู่แล้วและคุณไม่ต้องการเรียนรู้เทคโนโลยีใหม่ ๆ โดยมองหาวิธีแก้ปัญหาทุกที่ (และอาจมีสิ่งที่ไม่ถูกต้อง) คุณต้องแน่ใจว่าคุณกำลังเรียนรู้วิธีการที่ถูกต้อง
แน่นอนว่ามีบทความ JavaScript-Learning ที่ยอดเยี่ยมมากมายด้านนอก อย่างไรก็ตามบางครั้งมันก็ยังห่างไกลพอที่จะพึ่งพาบทความเหล่านั้นเพียงอย่างเดียว สิ่งที่คุณต้องการคือคำแนะนำ
เป้าหมายของหนังสือเล่มนี้คือการให้คำแนะนำแก่คุณ
แถลงการณ์สั้น ๆ
มีโปรแกรมเมอร์ JavaScript ที่ดีมากในอุตสาหกรรม และฉันไม่ใช่หนึ่งในนั้น
ฉันเป็นคนที่อธิบายไว้ในส่วนก่อนหน้า ฉันคุ้นเคยกับวิธีการพัฒนาแอปพลิเคชันเว็บแบ็กเอนด์ แต่ฉันเป็นแค่มือใหม่ที่มี JavaScript และ Node.js ฉันเพิ่งได้เรียนรู้แนวคิด JavaScript ขั้นสูงและไม่มีประสบการณ์ในทางปฏิบัติ
ดังนั้นหนังสือเล่มนี้ไม่ใช่หนังสือที่ "จากเบื้องต้นถึงความเชี่ยวชาญ" แต่เป็นเหมือนหนังสือที่ "จากเบื้องต้นถึงขั้นสูง"
หากประสบความสำเร็จหนังสือเล่มนี้เป็นบทช่วยสอนที่ฉันหวังไว้มากที่สุดเมื่อฉันเริ่มเรียนรู้โหนด
เซิร์ฟเวอร์จาวาสคริปต์
JavaScript ถูกเรียกใช้ครั้งแรกในเบราว์เซอร์อย่างไรก็ตามเบราว์เซอร์เพียงแค่ให้บริบทที่กำหนดสิ่งที่สามารถทำได้ด้วย JavaScript แต่ไม่ได้ "พูด" มากเกี่ยวกับสิ่งที่ภาษา JavaScript สามารถทำได้ ในความเป็นจริง JavaScript เป็นภาษา "สมบูรณ์": สามารถใช้ในบริบทที่แตกต่างกันโดยมีความสามารถยิ่งใหญ่กว่าภาษาอื่น ๆ ที่คล้ายกัน
Node.js เป็นอีกบริบทอื่นซึ่งอนุญาตให้ใช้รหัส JavaScript บนแบ็กเอนด์ (ออกจากสภาพแวดล้อมของเบราว์เซอร์)
ในการใช้รหัส JavaScript ที่ทำงานอยู่ในพื้นหลังรหัสจะต้องตีความก่อนแล้วจึงดำเนินการอย่างถูกต้อง นี่คือหลักการของ Node.js ซึ่งใช้เครื่องเสมือน V8 ของ Google (สภาพแวดล้อมการดำเนินการ JavaScript ที่ใช้โดย Chrome ของ Google) เพื่อตีความและเรียกใช้รหัส JavaScript
นอกจากนี้ยังมีโมดูลที่มีประโยชน์มากมายที่มาพร้อมกับ node.js ที่สามารถลดความซับซ้อนของงานซ้ำ ๆ เช่นการส่งออกสตริงไปยังเทอร์มินัล
ดังนั้น Node.js จึงเป็นทั้งสภาพแวดล้อมรันไทม์และห้องสมุด
ในการใช้ node.js คุณต้องติดตั้งก่อน ฉันจะไม่เข้าไปดูรายละเอียดเกี่ยวกับวิธีการติดตั้ง node.js ที่นี่ หลังจากการติดตั้งเสร็จสมบูรณ์ให้กลับมาอีกครั้งเพื่ออ่านเนื้อหาด้านล่างหนังสือเล่มนี้
“ สวัสดีโลก”
โอเคอย่าพูดว่า "ไร้สาระ" มากนักเรามาเริ่มแอปพลิเคชัน Node.js แรกของเรา: "Hello World" ทันที
เปิดตัวแก้ไขที่คุณชื่นชอบและสร้างไฟล์ helloWorld.js เราต้องทำเพื่อส่งออก "Hello World" เพื่อ stdout ดังนี้:
การคัดลอกรหัสมีดังนี้: console.log ("Hello World");
บันทึกไฟล์และดำเนินการผ่าน node.js:
คัดลอกรหัสดังนี้: Node HelloWorld.js
ถ้าปกติ Hello World จะถูกส่งไปยังเทอร์มินัล
ตกลงฉันยอมรับว่าแอปพลิเคชันนี้น่าเบื่อนิดหน่อยดังนั้นลองใช้ "แห้ง" นิดหน่อย
เว็บแอปพลิเคชันที่สมบูรณ์ตาม Node.js
ใช้เคส
มาตั้งเป้าหมายที่ง่าย แต่ต้องใช้งานได้จริง:
1. ผู้ใช้สามารถใช้แอพของเราผ่านเบราว์เซอร์
2. เมื่อผู้ใช้ร้องขอ http: // domain/start เขาสามารถดูหน้าต้อนรับด้วยแบบฟอร์มอัปโหลดไฟล์บนหน้า
3. ผู้ใช้สามารถเลือกรูปภาพและส่งแบบฟอร์มได้
เกือบจะเสร็จแล้วคุณสามารถไปที่ Google ได้แล้วและหาบางอย่างที่จะยุ่งเพื่อให้ฟังก์ชั่นเสร็จสมบูรณ์ แต่ตอนนี้เราจะไม่ทำสิ่งนี้
ต่อไปในกระบวนการของการบรรลุเป้าหมายนี้เราต้องการมากกว่ารหัสพื้นฐานโดยไม่คำนึงว่ารหัสนั้นสง่างามหรือไม่ นอกจากนี้เรายังต้องนามธรรมสิ่งนี้เพื่อหาวิธีสร้างแอปพลิเคชัน Node.js ที่ซับซ้อนมากขึ้น
แอปพลิเคชันของโมดูลที่แตกต่างกัน
มาทำลายแอปพลิเคชันนี้
1. เราจำเป็นต้องจัดเตรียมหน้าเว็บดังนั้นเราจึงต้องมีเซิร์ฟเวอร์ HTTP
2. สำหรับคำขอที่แตกต่างกันเซิร์ฟเวอร์ของเราจำเป็นต้องให้คำตอบที่แตกต่างกันตาม URL คำขอดังนั้นเราจึงต้องการเส้นทางที่จะสอดคล้องกับตัวจัดการคำขอ
3. เมื่อเซิร์ฟเวอร์ได้รับการร้องขอและผ่านเส้นทางจะต้องดำเนินการประมวลผลดังนั้นเราจึงต้องการตัวจัดการคำขอสุดท้าย
4. การกำหนดเส้นทางควรจะสามารถประมวลผลข้อมูลโพสต์และห่อหุ้มข้อมูลในรูปแบบที่เป็นมิตรมากขึ้นและส่งผ่านไปยังการประมวลผลคำขอลงในโปรแกรมดังนั้นจึงจำเป็นต้องใช้ฟังก์ชั่นการประมวลผลข้อมูลคำขอ
5. เราไม่เพียง แต่ต้องประมวลผลคำขอที่สอดคล้องกับ URL แต่ยังแสดงเนื้อหาซึ่งหมายความว่าเราต้องการตรรกะมุมมองสำหรับตัวจัดการคำขอเพื่อส่งเนื้อหาไปยังเบราว์เซอร์ของผู้ใช้
6. ในที่สุดผู้ใช้จำเป็นต้องอัปโหลดรูปภาพดังนั้นเราจำเป็นต้องอัปโหลดฟังก์ชั่นการประมวลผลเพื่อจัดการรายละเอียดของด้านนี้
ก่อนอื่นลองคิดดูว่าเราจะสร้างโครงสร้างนี้อย่างไรถ้าเราใช้ PHP โดยทั่วไปแล้วเราจะใช้เซิร์ฟเวอร์ Apache HTTP และจับคู่กับโมดูล mod_php5
จากมุมมองนี้ข้อกำหนดทั้งหมดของ "การรับคำขอ HTTP และการให้บริการเว็บเพจ" ไม่จำเป็นต้องใช้ PHP เลย
อย่างไรก็ตามสำหรับ node.js แนวคิดนั้นแตกต่างอย่างสิ้นเชิง เมื่อใช้ node.js เราไม่เพียง แต่ใช้แอปพลิเคชันเดียวเท่านั้น แต่ยังใช้เซิร์ฟเวอร์ HTTP ทั้งหมดด้วย ในความเป็นจริงเว็บแอปพลิเคชันของเราและเว็บเซิร์ฟเวอร์ที่เกี่ยวข้องนั้นเหมือนกัน
ดูเหมือนว่าจะมีงานมากมายให้ทำ แต่เราจะค่อยๆตระหนักว่านี่ไม่ใช่ความยุ่งยากสำหรับ Node.js.
ทีนี้มาเริ่มเส้นทางการใช้งานเริ่มต้นด้วยส่วนแรก - เซิร์ฟเวอร์ HTTP
สร้างโมดูลสำหรับแอปพลิเคชัน
เซิร์ฟเวอร์ HTTP พื้นฐาน
เมื่อฉันกำลังจะเริ่มเขียนแอปพลิเคชัน "จริง" ครั้งแรกของฉันฉันไม่เพียง แต่ไม่รู้วิธีเขียนรหัส node.js แต่ยังรวมถึงวิธีการจัดระเบียบด้วย
ฉันควรใส่ทุกอย่างในไฟล์เดียวหรือไม่? มีบทเรียนออนไลน์มากมายที่จะสอนให้คุณใส่ตรรกะทั้งหมดลงในเซิร์ฟเวอร์ HTTP พื้นฐานที่เขียนใน Node.js แต่ถ้าฉันต้องการเพิ่มเนื้อหาเพิ่มเติมในขณะเดียวกันก็ทำให้รหัสอ่านได้
ในความเป็นจริงมันค่อนข้างง่ายที่จะแยกรหัสออกตราบเท่าที่คุณใส่รหัสของฟังก์ชั่นที่แตกต่างกันลงในโมดูลที่แตกต่างกัน
วิธีนี้ช่วยให้คุณมีไฟล์หลักที่สะอาดซึ่งคุณสามารถเรียกใช้งานด้วย node.js;
ดังนั้นตอนนี้เรามาสร้างไฟล์หลักสำหรับการเริ่มต้นแอปพลิเคชันของเราและโมดูลที่ถือรหัสเซิร์ฟเวอร์ HTTP ของเรา
ในความประทับใจของฉันการเรียกไฟล์หลัก index.js เป็นรูปแบบมาตรฐานมากหรือน้อย เป็นเรื่องง่ายที่จะเข้าใจการใส่โมดูลเซิร์ฟเวอร์ลงในไฟล์ที่เรียกว่า Server.js
เริ่มต้นด้วยโมดูลเซิร์ฟเวอร์กันเถอะ สร้างไฟล์ที่เรียกว่า server.js ในไดเรกทอรีรูทของโครงการของคุณและเขียนรหัสต่อไปนี้:
การคัดลอกรหัสมีดังนี้:
var http = ต้องการ ("http");
http.createserver (ฟังก์ชั่น (คำขอ, การตอบกลับ) {
Response.writehead (200, {"เนื้อหาประเภท": "ข้อความ/ธรรมดา"});
Response.write ("Hello World");
Response.end ();
}). ฟัง (8888);
ทำเสร็จแล้ว! คุณเพิ่งเสร็จสิ้นเซิร์ฟเวอร์ HTTP ที่ใช้งานได้ เพื่อพิสูจน์สิ่งนี้ให้เรียกใช้และทดสอบรหัสนี้กันเถอะ ก่อนอื่นให้ดำเนินการสคริปต์ของคุณด้วย node.js:
Node Server.js
ถัดไปเปิดเบราว์เซอร์และเยี่ยมชม http: // localhost: 8888/และคุณจะเห็นหน้าเว็บที่มี "Hello World" เขียนไว้
นี่น่าสนใจใช่มั้ย พูดคุยเกี่ยวกับปัญหาของเซิร์ฟเวอร์ HTTP ก่อนวางเรื่องของวิธีการจัดระเบียบโครงการกัน ฉันสัญญาว่าเราจะแก้ปัญหานั้นในภายหลัง
วิเคราะห์เซิร์ฟเวอร์ HTTP
จากนั้นมาวิเคราะห์องค์ประกอบของเซิร์ฟเวอร์ HTTP นี้
คำขอบรรทัดแรก (ต้องการ) โมดูล HTTP ที่มาพร้อมกับ node.js และกำหนดให้กับตัวแปร HTTP
ต่อไปเราเรียกฟังก์ชั่นที่จัดทำโดยโมดูล HTTP: CreateServer ฟังก์ชั่นนี้จะส่งคืนวัตถุซึ่งมีวิธีการฟัง
ลองเพิกเฉยต่อคำจำกัดความของฟังก์ชั่นในวงเล็บของ http.createserver ในขณะนี้
เราสามารถใช้รหัสเช่นนี้เพื่อเริ่มเซิร์ฟเวอร์และฟังพอร์ต 8888:
การคัดลอกรหัสมีดังนี้:
var http = ต้องการ ("http");
var server = http.createServer ();
Server.Listen (8888);
รหัสนี้จะเริ่มต้นเซิร์ฟเวอร์ที่รับฟังพอร์ต 8888 เท่านั้นและจะไม่ทำอะไรเลยและจะไม่ตอบคำขอ
สิ่งที่น่าสนใจที่สุด (และถ้าคุณคุ้นเคยกับการใช้ภาษาที่อนุรักษ์นิยมมากขึ้นเช่น PHP มันแปลก) เป็นอาร์กิวเมนต์แรกในการสร้าง () คำจำกัดความของฟังก์ชั่น
ในความเป็นจริงคำจำกัดความของฟังก์ชั่นนี้เป็นพารามิเตอร์แรกและเดียวของ CreateServer () เพราะใน JavaScript ฟังก์ชั่นสามารถส่งผ่านเหมือนตัวแปรอื่น ๆ
ดำเนินการฟังก์ชัน Pass
ตัวอย่างเช่นคุณสามารถทำได้:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชั่นบอกว่า (คำ) {
console.log (คำ);
-
ฟังก์ชั่นดำเนินการ (somefunction, value) {
บาง function (ค่า);
-
ดำเนินการ (พูดว่า "สวัสดี");
โปรดอ่านรหัสนี้อย่างระมัดระวัง! ที่นี่เราผ่านฟังก์ชั่น SAY เป็นตัวแปรแรกของฟังก์ชัน EXECUTE สิ่งที่ถูกส่งคืนที่นี่ไม่ใช่ค่าตอบแทนของการพูด แต่คำพูดของตัวเอง!
ด้วยวิธีนี้การพูดจะกลายเป็นตัวแปรท้องถิ่นบางอย่างในการดำเนินการ
แน่นอนเพราะ Say มีตัวแปรการดำเนินการสามารถผ่านตัวแปรดังกล่าวได้เมื่อโทรหาบางอย่าง
ตอนนี้เราสามารถผ่านฟังก์ชั่นเป็นตัวแปรที่มีชื่อได้ แต่เราไม่ต้องวนเวียนอยู่รอบ ๆ "กำหนดก่อนจากนั้นผ่าน" วงกลม
การคัดลอกรหัสมีดังนี้:
ฟังก์ชั่นดำเนินการ (somefunction, value) {
บาง function (ค่า);
-
ดำเนินการ (ฟังก์ชัน (word) {console.log (word)}, "hello");
เรากำหนดฟังก์ชั่นโดยตรงที่เราจะส่งผ่านเพื่อดำเนินการโดยที่พารามิเตอร์แรกได้รับการยอมรับโดยการดำเนินการ
ด้วยวิธีนี้เราไม่จำเป็นต้องตั้งชื่อฟังก์ชั่นนี้ซึ่งเป็นสาเหตุที่เรียกว่าฟังก์ชันที่ไม่ระบุชื่อ
นี่คือการติดต่ออย่างใกล้ชิดครั้งแรกของเรากับสิ่งที่ฉันคิดว่าเป็น "ขั้นสูง" JavaScript แต่เรายังต้องก้าวไปทีละขั้นตอน ตอนนี้เรามายอมรับสิ่งนี้ก่อน: ใน JavaScript ฟังก์ชั่นสามารถรับพารามิเตอร์เป็นฟังก์ชั่นอื่นได้ ก่อนอื่นเราสามารถกำหนดฟังก์ชั่นแล้วส่งผ่านหรือเราสามารถกำหนดฟังก์ชั่นโดยตรงที่พารามิเตอร์ถูกส่งผ่าน
ฟังก์ชั่นส่งผ่านผ่านเซิร์ฟเวอร์ HTTP ทำงานอย่างไร
ด้วยความรู้นี้มาดูเซิร์ฟเวอร์ HTTP ที่เรียบง่าย แต่ไม่ใช่เรื่องง่าย:
การคัดลอกรหัสมีดังนี้:
var http = ต้องการ ("http");
http.createserver (ฟังก์ชั่น (คำขอ, การตอบกลับ) {
Response.writehead (200, {"เนื้อหาประเภท": "ข้อความ/ธรรมดา"});
Response.write ("Hello World");
Response.end ();
}). ฟัง (8888);
ตอนนี้มันควรดูชัดเจนขึ้นมาก: เราผ่านฟังก์ชั่นที่ไม่ระบุชื่อไปยังฟังก์ชั่นผู้สร้าง
จุดประสงค์เดียวกันสามารถทำได้โดยใช้รหัสดังกล่าว:
การคัดลอกรหัสมีดังนี้:
var http = ต้องการ ("http");
ฟังก์ชั่น onRequest (คำขอการตอบกลับ) {
Response.writehead (200, {"เนื้อหาประเภท": "ข้อความ/ธรรมดา"});
Response.write ("Hello World");
Response.end ();
-
http.createserver (onrequest) .Listen (8888);
บางทีตอนนี้เราควรถามคำถามนี้: ทำไมเราถึงใช้วิธีนี้?
การโทรกลับที่ขับเคลื่อนด้วยเหตุการณ์
คำถามนี้ยากที่จะตอบ (อย่างน้อยสำหรับฉัน) แต่นี่คือวิธีที่ node.js ทำงานโดยธรรมชาติ เป็นเหตุการณ์ที่ขับเคลื่อนด้วยซึ่งเป็นเหตุผลว่าทำไมมันถึงเร็วมาก
คุณอาจต้องการใช้เวลาสักครู่ในการอ่านผลงานชิ้นเอกของ Felix Geisendörferทำความเข้าใจ Node.js ซึ่งแนะนำความรู้พื้นฐานบางอย่าง
ทุกอย่างลงมาจากความจริงที่ว่า "node.js เป็นเหตุการณ์ขับเคลื่อน" ฉันไม่เข้าใจความหมายของประโยคนี้จริงๆ แต่ฉันจะพยายามอธิบายว่าทำไมเราถึงเขียนเว็บแอปพลิเคชันโดยใช้ node.js
เมื่อเราใช้เมธอด http.createserver เราไม่เพียง แต่ต้องการเซิร์ฟเวอร์ที่รับฟังพอร์ตหนึ่งเรายังต้องการให้ทำอะไรบางอย่างเมื่อเซิร์ฟเวอร์ได้รับคำขอ HTTP
ปัญหาคือนี่เป็นแบบอะซิงโครนัส: คำขอสามารถมาถึงได้ตลอดเวลา แต่เซิร์ฟเวอร์ของเราทำงานในกระบวนการเดียว
เมื่อเขียนแอปพลิเคชัน PHP เราไม่กังวลเกี่ยวกับเรื่องนี้เลย: เมื่อใดก็ตามที่คำขอเข้าสู่เว็บเซิร์ฟเวอร์ (โดยปกติ Apache) จะสร้างกระบวนการใหม่สำหรับคำขอและเริ่มดำเนินการสคริปต์ PHP ที่เกี่ยวข้องตั้งแต่ต้นจนจบ
ดังนั้นในโปรแกรม Node.js ของเราเมื่อคำขอใหม่มาถึงพอร์ต 8888 เราจะควบคุมกระบวนการได้อย่างไร
นั่นคือสิ่งที่การออกแบบที่ขับเคลื่อนด้วยเหตุการณ์ของ Node.js/JavaScript สามารถช่วยได้จริงๆ - แม้ว่าเราจะยังต้องเรียนรู้แนวคิดใหม่ ๆ เพื่อฝึกฝน มาดูกันว่าแนวคิดเหล่านี้ถูกนำไปใช้กับรหัสเซิร์ฟเวอร์ของเราอย่างไร
เราสร้างเซิร์ฟเวอร์และส่งผ่านฟังก์ชั่นไปยังวิธีการที่สร้างขึ้น เมื่อใดก็ตามที่เซิร์ฟเวอร์ของเราได้รับการร้องขอฟังก์ชั่นนี้จะถูกเรียก
เราไม่รู้ว่าสิ่งนี้จะเกิดขึ้นเมื่อใด แต่ตอนนี้เรามีสถานที่สำหรับจัดการคำขอ: มันเป็นฟังก์ชั่นที่เราผ่านมาในอดีต ไม่ว่าจะเป็นฟังก์ชั่นที่กำหนดไว้ล่วงหน้าหรือฟังก์ชั่นที่ไม่ระบุชื่อก็ไม่สำคัญ
นี่คือการโทรกลับในตำนาน เราส่งฟังก์ชั่นไปยังวิธีการซึ่งเรียกใช้ฟังก์ชันนี้เพื่อโทรกลับเมื่อเหตุการณ์ที่เกี่ยวข้องเกิดขึ้น
อย่างน้อยสำหรับฉันมันต้องใช้ความพยายามในการทำความเข้าใจ หากคุณยังไม่แน่ใจอ่านโพสต์บล็อกของ Felix
ลองคิดเกี่ยวกับแนวคิดใหม่นี้อีกครั้ง เราจะพิสูจน์ได้อย่างไรว่าหลังจากสร้างเซิร์ฟเวอร์รหัสของเราจะยังคงถูกต้องแม้ว่าจะไม่มีคำขอ HTTP เข้ามาและฟังก์ชั่นการโทรกลับของเราไม่ได้เรียก? ลองกันเถอะ:
การคัดลอกรหัสมีดังนี้:
var http = ต้องการ ("http");
ฟังก์ชั่น onRequest (คำขอการตอบกลับ) {
console.log ("ได้รับคำขอ");
Response.writehead (200, {"เนื้อหาประเภท": "ข้อความ/ธรรมดา"});
Response.write ("Hello World");
Response.end ();
-
http.createserver (onrequest) .Listen (8888);
console.log ("เซิร์ฟเวอร์เริ่มต้นแล้ว");
หมายเหตุ: เมื่อใดที่ OnRequest (ฟังก์ชั่นการโทรกลับของเรา) ถูกเรียกใช้ฉันส่งออกข้อความโดยใช้ console.log หลังจากเซิร์ฟเวอร์ HTTP เริ่มทำงานแล้วชิ้นส่วนของข้อความก็จะถูกส่งออก
เมื่อเราเรียกใช้ Node Server.js ตามปกติจะส่งออกทันที "เซิร์ฟเวอร์เริ่มต้น" บนบรรทัดคำสั่ง เมื่อเราร้องขอไปยังเซิร์ฟเวอร์ (เยี่ยมชม http: // localhost: 8888/ในเบราว์เซอร์) ข้อความ "คำขอที่ได้รับ" จะปรากฏบนบรรทัดคำสั่ง
นี่คือ JavaScript ฝั่งเซิร์ฟเวอร์ที่ขับเคลื่อนด้วยเหตุการณ์และการโทรกลับ!
(โปรดทราบว่าเมื่อเราเข้าถึงเว็บเพจบนเซิร์ฟเวอร์เซิร์ฟเวอร์ของเราอาจส่งออก "คำขอ" สองครั้งนั่นเป็นเพราะเซิร์ฟเวอร์ส่วนใหญ่จะพยายามอ่าน http เมื่อคุณเยี่ยมชม http: // localhost: 8888/: // localhost: 8888 /favicon.ico)
เซิร์ฟเวอร์จัดการคำขออย่างไร
ตกลงวิเคราะห์ส่วนที่เหลือในรหัสเซิร์ฟเวอร์ของเราสั้น ๆ ซึ่งเป็นส่วนหลักของฟังก์ชั่นการโทรกลับของเรา OnRequest ()
เมื่อการโทรกลับเริ่มต้นและฟังก์ชั่น onRequest () ของเราจะถูกเรียกใช้พารามิเตอร์สองตัวจะถูกส่งผ่าน: คำขอและการตอบสนอง
พวกเขาเป็นวัตถุคุณสามารถใช้วิธีการของพวกเขาเพื่อจัดการรายละเอียดของคำขอ HTTP และตอบสนองต่อคำขอ (เช่นส่งบางสิ่งบางอย่างกลับไปยังเบราว์เซอร์ที่ทำคำขอ)
ดังนั้นรหัสของเราคือ: เมื่อได้รับการร้องขอให้ใช้ฟังก์ชัน Response.writehead () เพื่อส่งสถานะ HTTP 200 และเนื้อหาประเภทของส่วนหัว HTTP และใช้ฟังก์ชัน Response.write () เพื่อส่งข้อความในตัว HTTP ที่สอดคล้องกัน "สวัสดีโลก"
ในที่สุดเราเรียกตอบกลับ () เพื่อตอบสนองให้เสร็จสมบูรณ์
ในขณะนี้เราไม่สนใจรายละเอียดของคำขอดังนั้นเราจึงไม่ได้ใช้วัตถุคำขอ
จะวางโมดูลเซิร์ฟเวอร์ได้ที่ไหน
ตกลงเหมือนที่ฉันสัญญาไว้ตอนนี้เราสามารถย้อนกลับไปที่วิธีที่เราจัดระเบียบแอปพลิเคชัน ตอนนี้เรามีรหัสเซิร์ฟเวอร์ HTTP พื้นฐานในไฟล์ Server.js และฉันบอกว่าเรามักจะมีไฟล์ที่เรียกว่า index.js เพื่อเรียกโมดูลอื่น ๆ ของแอปพลิเคชัน (เช่นโมดูลเซิร์ฟเวอร์ HTTP ในเซิร์ฟเวอร์ เริ่มแอปพลิเคชัน
มาพูดถึงวิธีการเปลี่ยนเซิร์ฟเวอร์ js ให้เป็นโมดูล node.js จริงเพื่อให้สามารถใช้งานได้ (ยังไม่เริ่มต้น) index.js ไฟล์หลัก
บางทีคุณอาจสังเกตเห็นว่าเราใช้โมดูลในรหัสของเรา แบบนี้:
การคัดลอกรหัสมีดังนี้:
var http = ต้องการ ("http");
-
http.createserver (... );
Node.js มาพร้อมกับโมดูลที่เรียกว่า "HTTP"
สิ่งนี้จะเปลี่ยนตัวแปรท้องถิ่นของเราให้กลายเป็นวัตถุที่มีวิธีการสาธารณะทั้งหมดที่จัดทำโดยโมดูล HTTP
เป็นการประชุมที่จะให้ชื่อตัวแปรในเครื่องนี้ซึ่งเหมือนกับชื่อโมดูล แต่คุณสามารถติดตามการตั้งค่าของคุณได้:
การคัดลอกรหัสมีดังนี้:
var foo = ต้องการ ("http");
-
foo.createserver (... );
ดีมากมันชัดเจนว่าจะใช้โมดูลภายใน node.js ได้อย่างไร เราจะสร้างโมดูลของเราเองได้อย่างไรและเราจะใช้มันอย่างไร?
เมื่อเราเปลี่ยน Server.js เป็นโมดูลจริงคุณจะเข้าใจ
ในความเป็นจริงเราไม่จำเป็นต้องทำการปรับเปลี่ยนมากเกินไป การเปลี่ยนชิ้นส่วนของรหัสเป็นโมดูลหมายความว่าเราจำเป็นต้องส่งออกชิ้นส่วนที่เราต้องการให้การใช้งานกับสคริปต์ที่ร้องขอโมดูล
ในปัจจุบันฟังก์ชั่นที่เซิร์ฟเวอร์ HTTP ของเราจำเป็นต้องส่งออกนั้นง่ายมากเนื่องจากสคริปต์ที่ร้องขอโมดูลเซิร์ฟเวอร์ต้องเริ่มต้นเซิร์ฟเวอร์เท่านั้น
เราใส่สคริปต์เซิร์ฟเวอร์ของเราลงในฟังก์ชั่นที่เรียกว่า Start และเราจะส่งออกฟังก์ชั่นนี้
การคัดลอกรหัสมีดังนี้:
var http = ต้องการ ("http");
ฟังก์ชั่นเริ่มต้น () {
ฟังก์ชั่น onRequest (คำขอการตอบกลับ) {
console.log ("ได้รับคำขอ");
Response.writehead (200, {"เนื้อหาประเภท": "ข้อความ/ธรรมดา"});
Response.write ("Hello World");
Response.end ();
-
http.createserver (onrequest) .Listen (8888);
console.log ("เซิร์ฟเวอร์เริ่มต้นแล้ว");
-
ExportS.Start = Start;
ด้วยวิธีนี้ตอนนี้เราสามารถสร้างไฟล์หลักของเรา index.js และเริ่ม HTTP ของเราในนั้นแม้ว่ารหัสของเซิร์ฟเวอร์จะยังอยู่ใน Server.js
สร้างไฟล์ index.js และเขียนสิ่งต่อไปนี้:
การคัดลอกรหัสมีดังนี้:
var server = ต้องการ ("./ เซิร์ฟเวอร์");
Server.start ();
อย่างที่คุณเห็นเราสามารถใช้โมดูลเซิร์ฟเวอร์เช่นโมดูลในตัวอื่น ๆ : ขอไฟล์นี้และชี้ไปที่ตัวแปรที่เราสามารถใช้ฟังก์ชั่นที่ส่งออกได้
ใช้ได้. ตอนนี้เราสามารถเปิดแอปพลิเคชันของเราจากสคริปต์หลักของเราและมันก็ยังเหมือนกัน:
การคัดลอกรหัสมีดังนี้:
Node Index.js
ดีมากตอนนี้เราสามารถใส่ส่วนต่าง ๆ ของแอปพลิเคชันของเราลงในไฟล์ต่าง ๆ และเชื่อมต่อเข้าด้วยกันโดยสร้างโมดูล
เรายังคงมีเพียงส่วนแรกของแอปพลิเคชันทั้งหมด: เราสามารถรับคำขอ HTTP ได้ แต่เราต้องทำอะไรบางอย่าง - เซิร์ฟเวอร์ควรมีปฏิกิริยาที่แตกต่างกันต่อคำขอ URL ที่แตกต่างกัน
สำหรับแอปพลิเคชันที่ง่ายมากคุณสามารถทำได้โดยตรงในฟังก์ชั่นการโทรกลับ onrequest () แต่อย่างที่ฉันพูดเราควรเพิ่มองค์ประกอบนามธรรมบางอย่างเพื่อให้ตัวอย่างของเราน่าสนใจขึ้นเล็กน้อย
การจัดการคำขอ HTTP ที่แตกต่างกันเป็นส่วนที่แตกต่างกันในรหัสของเราที่เรียกว่า "การกำหนดเส้นทาง" - ดังนั้นเรามาสร้างโมดูลที่เรียกว่าการกำหนดเส้นทาง
วิธีทำคำขอ "การกำหนดเส้นทาง"
เราต้องการจัดเตรียม URL ที่ร้องขอและพารามิเตอร์ GET และโพสต์อื่น ๆ ที่จำเป็นสำหรับเส้นทางจากนั้นเส้นทางจะต้องเรียกใช้รหัสที่เกี่ยวข้องตามข้อมูลนี้ (นี่คือ "รหัส" ที่สอดคล้องกับส่วนที่สามของแอปพลิเคชันทั้งหมด: ซีรีส์ ของการทำงานจริงเมื่อได้รับตัวจัดการคำขอ)
ดังนั้นเราจำเป็นต้องดูคำขอ HTTP แยก URL ที่ร้องขอออกจากมันและพารามิเตอร์ GET/POST หากฟังก์ชั่นนี้เป็นของการกำหนดเส้นทางหรือเซิร์ฟเวอร์ (แม้เป็นฟังก์ชันของโมดูลเอง) แต่มันก็เป็นฟังก์ชั่นของเซิร์ฟเวอร์ HTTP ของเราอย่างไม่แน่นอน
ข้อมูลทั้งหมดที่เราต้องการรวมอยู่ในวัตถุคำขอซึ่งผ่านเป็นพารามิเตอร์แรกของฟังก์ชันการโทรกลับ onrequest () แต่เพื่อแยกวิเคราะห์ข้อมูลนี้เราจำเป็นต้องมีโมดูล node.js เพิ่มเติมซึ่งเป็น URL และโมดูล QueryString ตามลำดับ
การคัดลอกรหัสมีดังนี้:
url.parse (สตริง). QUARY
url.parse (สตริง) .pathName |
-
-
http: // localhost: 8888/start? foo = bar & hello = world
-
-
QueryString (สตริง) ["foo"] |
QueryString (สตริง) ["สวัสดี"]
แน่นอนว่าเรายังสามารถใช้โมดูล QueryString เพื่อแยกวิเคราะห์พารามิเตอร์ในร่างกายคำขอโพสต์และจะมีการสาธิตในภายหลัง
ตอนนี้เรามาเพิ่มตรรกะลงในฟังก์ชัน OnRequest () เพื่อค้นหาเส้นทาง URL ที่เบราว์เซอร์ร้องขอ:
var http = ต้องการ ("http");
var url = ต้องการ ("url");
ฟังก์ชั่นเริ่มต้น () {
ฟังก์ชั่น onRequest (คำขอการตอบกลับ) {
var pathName = url.parse (request.url) .pathName;
console.log ("ขอ" + pathname + "ได้รับ");
Response.writehead (200, {"เนื้อหาประเภท": "ข้อความ/ธรรมดา"});
Response.write ("Hello World");
Response.end ();
-
http.createserver (onrequest) .Listen (8888);
console.log ("เซิร์ฟเวอร์เริ่มต้นแล้ว");
-
ExportS.Start = Start;
ตอนนี้แอปของเราสามารถแยกแยะคำขอที่แตกต่างกันได้โดยเส้นทาง URL ที่ร้องขอ - ซึ่งช่วยให้เราสามารถแมปคำขอไปยังตัวจัดการโดยใช้เส้นทาง (ยังไม่เสร็จ) เพื่อยึดคำขอบนเส้นทาง URL
ในแอปพลิเคชันเรากำลังสร้างซึ่งหมายความว่าคำขอจาก /เริ่มต้นและ /อัปโหลดสามารถจัดการได้ในรหัสที่แตกต่างกัน เราจะดูว่าเนื้อหานี้ถูกนำมารวมกันอย่างไรในภายหลัง
ตอนนี้เราสามารถเขียนเส้นทางสร้างไฟล์ที่เรียกว่า Router.js และเพิ่มสิ่งต่อไปนี้:
เส้นทางฟังก์ชัน (ชื่อพา ธ ) {
console.log ("เกี่ยวกับการกำหนดเส้นทางสำหรับ" + ชื่อพา ธ );
-
exports.route = เส้นทาง;
อย่างที่คุณเห็นรหัสนี้ไม่ได้ทำอะไรเลย แต่ตอนนี้มันเป็นสิ่งที่ควรจะเป็น ก่อนที่จะเพิ่มตรรกะเพิ่มเติมก่อนอื่นมาดูวิธีการรวมการกำหนดเส้นทางและเซิร์ฟเวอร์
เซิร์ฟเวอร์ของเราควรรู้การมีอยู่ของเส้นทางและใช้งานได้อย่างมีประสิทธิภาพ แน่นอนว่าเราสามารถผูกมัดการพึ่งพานี้กับเซิร์ฟเวอร์ได้ด้วยการเข้ารหัสแบบแข็ง แต่ประสบการณ์การเขียนโปรแกรมในภาษาอื่น ๆ บอกเราว่านี่จะเป็นสิ่งที่เจ็บปวดมากดังนั้นเราจะใช้การฉีดพึ่งพาเพื่อเพิ่มเส้นทางมากขึ้นโมดูล (คุณสามารถอ่านได้ ผลงานชิ้นเอกของ Martin Fowlers เกี่ยวกับการฉีดพึ่งพาเป็นความรู้พื้นฐาน)
ก่อนอื่นให้ขยายฟังก์ชั่นเริ่มต้น () ของเซิร์ฟเวอร์เพื่อให้ฟังก์ชั่นการกำหนดเส้นทางถูกส่งเป็นพารามิเตอร์:
การคัดลอกรหัสมีดังนี้:
var http = ต้องการ ("http");
var url = ต้องการ ("url");
ฟังก์ชั่นเริ่มต้น (เส้นทาง) {
ฟังก์ชั่น onRequest (คำขอการตอบกลับ) {
var pathName = url.parse (request.url) .pathName;
console.log ("ขอ" + pathname + "ได้รับ");
เส้นทาง (ชื่อพา ธ );
Response.writehead (200, {"เนื้อหาประเภท": "ข้อความ/ธรรมดา"});
Response.write ("Hello World");
Response.end ();
-
http.createserver (onrequest) .Listen (8888);
console.log ("เซิร์ฟเวอร์เริ่มต้นแล้ว");
-
ExportS.Start = Start;
ในเวลาเดียวกันเราจะขยาย index.js ตามเพื่อให้ฟังก์ชันการกำหนดเส้นทางสามารถฉีดลงในเซิร์ฟเวอร์:
การคัดลอกรหัสมีดังนี้:
var server = ต้องการ ("./ เซิร์ฟเวอร์");
var เราเตอร์ = ต้องการ ("./ เราเตอร์");
Server.Start (Router.Route);
ที่นี่ฟังก์ชั่นที่เราผ่านไม่ได้ทำอะไรเลย
หากคุณเริ่มแอปพลิเคชันตอนนี้ (node index.js โปรดจำไว้เสมอว่าบรรทัดคำสั่งนี้) จากนั้นขอ URL คุณจะเห็นเอาต์พุตแอปพลิเคชันข้อมูลที่เกี่ยวข้องซึ่งระบุว่าเซิร์ฟเวอร์ HTTP ของเราใช้โมดูลการกำหนดเส้นทางและจะ ขอ
การคัดลอกรหัสมีดังนี้:
bash $ node index.js
ขอ /foo ได้รับ
กำลังจะกำหนดเส้นทางคำขอ /foo
(เอาต์พุตข้างต้นได้ลบชิ้นส่วนที่เกี่ยวข้องกับการร้องขอ /favicon.ico ที่น่ารำคาญมากขึ้น)
การดำเนินการที่ขับเคลื่อนด้วยพฤติกรรม
โปรดอนุญาตให้ฉันออกจากหัวข้ออีกครั้งและพูดคุยเกี่ยวกับการเขียนโปรแกรมที่ใช้งานได้ที่นี่
ฟังก์ชั่นการผ่านเป็นพารามิเตอร์ไม่ได้เป็นเพียงการพิจารณาทางเทคนิค สำหรับการออกแบบซอฟต์แวร์นี่เป็นคำถามเชิงปรัชญา ลองนึกถึงสถานการณ์นี้: ในไฟล์ดัชนีเราสามารถส่งวัตถุเราเตอร์ในและจากนั้นเซิร์ฟเวอร์สามารถเรียกใช้ฟังก์ชันเส้นทางของวัตถุนี้
เช่นนี้เราผ่านบางสิ่งบางอย่างและเซิร์ฟเวอร์ใช้สิ่งนี้เพื่อทำบางสิ่งให้สำเร็จ สวัสดีสิ่งที่เรียกว่าการกำหนดเส้นทางคุณช่วยฉันกำหนดเส้นทางนี้ได้ไหม
แต่เซิร์ฟเวอร์ไม่ต้องการสิ่งเหล่านี้ มันต้องทำสิ่งต่าง ๆ ให้เสร็จ นั่นคือคุณไม่ต้องการคำนามคุณต้องใช้คำกริยา
หลังจากทำความเข้าใจแนวคิดหลักและแนวคิดพื้นฐานที่สุดในแนวคิดนี้ฉันเข้าใจการเขียนโปรแกรมที่ใช้งานได้ตามธรรมชาติ
ฉันเข้าใจการเขียนโปรแกรมฟังก์ชั่นหลังจากอ่านผลงานชิ้นเอกของ Steve Yegge เรื่องการลงโทษประหารชีวิตในอาณาจักรแห่งคำนาม คุณไปอ่านหนังสือเล่มนี้จริงๆ นี่เป็นหนึ่งในหนังสือเกี่ยวกับซอฟต์แวร์ที่ฉันให้ความสุขกับการอ่าน
การกำหนดเส้นทางไปยังตัวจัดการคำขอจริง
กลับไปที่หัวข้อเซิร์ฟเวอร์ HTTP ของเราและโมดูลการขอเส้นทางเป็นตอนนี้ตามที่เราคาดไว้และสามารถสื่อสารกันได้เช่นพี่น้องสนิท
แน่นอนว่านี่เป็นสิ่งที่ไกลพอ ตัวอย่างเช่น "ตรรกะทางธุรกิจ" ของการประมวลผล/การเริ่มต้นควรแตกต่างจากการประมวลผล/อัปโหลด
ด้วยการใช้งานปัจจุบันกระบวนการกำหนดเส้นทางจะ "สิ้นสุด" ในโมดูลการกำหนดเส้นทางและโมดูลการกำหนดเส้นทางไม่ใช่โมดูลที่ "ดำเนินการ" จริง ๆ กับคำขอมิฉะนั้นจะไม่ดีมากเมื่อแอปพลิเคชันของเรามีความซับซ้อนมากขึ้น
เราเรียกฟังก์ชั่นชั่วคราวที่เป็นเป้าหมายการกำหนดเส้นทางสำหรับตัวจัดการคำขอ ตอนนี้เราไม่ควรรีบพัฒนาโมดูลการกำหนดเส้นทางเพราะหากตัวจัดการคำขอยังไม่พร้อมมันจะไม่สมเหตุสมผลในการปรับปรุงโมดูลการกำหนดเส้นทาง
แอปพลิเคชันต้องการส่วนประกอบใหม่ดังนั้นการเพิ่มโมดูลใหม่ - ไม่จำเป็นต้องแปลกใหม่อีกต่อไป มาสร้างโมดูลที่เรียกว่า requesthandlers และสำหรับตัวจัดการคำขอแต่ละตัวเพิ่มฟังก์ชั่นตัวยึดตำแหน่งจากนั้นส่งออกฟังก์ชั่นเหล่านี้เป็นวิธีโมดูล:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชั่นเริ่มต้น () {
console.log ("คำขอเริ่มต้น 'start' ถูกเรียก");
-
ฟังก์ชั่นอัปโหลด () {
console.log ("คำขอตัวจัดการ 'อัพโหลด' ถูกเรียกว่า");
-
ExportS.Start = Start;
ExportS.UPLOAD = อัปโหลด;
ด้วยวิธีนี้เราสามารถเชื่อมต่อตัวจัดการคำขอและโมดูลการกำหนดเส้นทางเพื่อสร้างเส้นทาง "มีวิธีค้นหา"
ที่นี่เราต้องตัดสินใจ: เราควรใช้รหัสฮาร์ดโมดูล Hardcode Module ในเส้นทางสำหรับการใช้งานหรือเพื่อเพิ่มการฉีดพึ่งพาเพิ่มขึ้นอีกเล็กน้อยหรือไม่? แม้ว่าจะเป็นโหมดอื่น ๆ การฉีดพึ่งพาไม่ควรใช้สำหรับใช้เท่านั้นในกรณีนี้การใช้การฉีดพึ่งพาสามารถคลายการมีเพศสัมพันธ์ระหว่างเส้นทางและตัวจัดการคำขอและทำให้เส้นทางสามารถนำกลับมาใช้ใหม่ได้มากขึ้น
ซึ่งหมายความว่าเราต้องผ่านตัวจัดการคำขอจากเซิร์ฟเวอร์ไปยังเส้นทาง แต่มันรู้สึกอุกอาจมากขึ้นที่จะทำสิ่งนี้ ไปยังเส้นทาง
แล้วเราจะผ่านตัวจัดการคำขอเหล่านี้ได้อย่างไร? แม้ว่าเราจะมีตัวจัดการเพียง 2 ตัวในแอปพลิเคชันจริงจำนวนผู้ดำเนินการตามคำขอจะเพิ่มขึ้นอย่างต่อเนื่อง ไปยังตัวจัดการซ้ำ ๆ นอกจากนี้ยังมีการร้องขอจำนวนมาก == x จากนั้นเรียกตัวจัดการ y ในเส้นทางซึ่งทำให้ระบบน่าเกลียด
ลองคิดดูอย่างระมัดระวังมีหลายสิ่งหลายอย่างซึ่งแต่ละสิ่งจะต้องแมปกับสตริง (นั่นคือ URL ที่ร้องขอ)? ดูเหมือนว่าอาร์เรย์ที่เชื่อมโยงจะมีความสามารถอย่างสมบูรณ์แบบ
แต่ผลลัพธ์ก็น่าผิดหวังเล็กน้อย JavaScript ไม่ได้จัดเตรียมอาร์เรย์แบบเชื่อมโยง - สามารถกล่าวได้ว่าจะให้พวกเขาได้หรือไม่? ในความเป็นจริงใน JavaScript สิ่งที่ให้ฟังก์ชั่นประเภทนี้คือวัตถุ
ในเรื่องนี้ http://msdn.microsoft.com/en-us/magazine/cc163419.aspx มีการแนะนำที่ดีและฉันจะตัดตอนมาที่นี่:
ใน C ++ หรือ C#เมื่อเราพูดถึงวัตถุเราอ้างถึงอินสแตนซ์ของคลาสหรือโครงสร้าง วัตถุจะมีคุณสมบัติและวิธีการที่แตกต่างกันตามเทมเพลตที่พวกเขาสร้างอินสแตนซ์ (นั่นคือคลาสที่เรียกว่า) แต่ในวัตถุ JavaScript ไม่ใช่แนวคิดนี้ ใน JavaScript วัตถุเป็นชุดของคู่คีย์/ค่า - คุณสามารถนึกถึงวัตถุ JavaScript เป็นพจนานุกรมที่มีคีย์เป็นประเภทสตริง
แต่ถ้าวัตถุ JavaScript เป็นเพียงคอลเลกชันของคู่คีย์/ค่ามันจะมีวิธีการได้อย่างไร? ค่าที่นี่อาจเป็นสตริงตัวเลขหรือ ... ฟังก์ชัน!
โอเคกลับไปที่รหัสในตอนท้าย现在我们已经确定将一系列请求处理程序通过一个对象来传递,并且需要使用松耦合的方式将这个对象注入到route()函数中。
我们先将这个对象引入到主文件index.js中:
การคัดลอกรหัสมีดังนี้:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
server.start(router.route, handle);
虽然handle并不仅仅是一个“东西”(一些请求处理程序的集合),我还是建议以一个动词作为其命名,这样做可以让我们在路由中使用更流畅的表达式,稍后会有说明。
正如所见,将不同的URL映射到相同的请求处理程序上是很容易的:只要在对象中添加一个键为"/"的属性,对应requestHandlers.start即可,这样我们就可以干净简洁地配置/start和/的请求都交由start这一处理程序处理。
在完成了对象的定义后,我们把它作为额外的参数传递给服务器,为此将server.js修改如下:
การคัดลอกรหัสมีดังนี้:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(handle, pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
-
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
-
exports.start = start;
这样我们就在start()函数里添加了handle参数,并且把handle对象作为第一个参数传递给了route()回调函数。
然后我们相应地在route.js文件中修改route()函数:
การคัดลอกรหัสมีดังนี้:
function route(handle, pathname) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname]();
} อื่น {
console.log("No request handler found for " + pathname);
-
-
exports.route = route;
通过以上代码,我们首先检查给定的路径对应的请求处理程序是否存在,如果存在的话直接调用相应的函数。我们可以用从关联数组中获取元素一样的方式从传递的对象中获取请求处理函数,因此就有了简洁流畅的形如handle[pathname]();的表达式,这个感觉就像在前方中提到的那样:“嗨,请帮我处理了这个路径”。
有了这些,我们就把服务器、路由和请求处理程序在一起了。现在我们启动应用程序并在浏览器中访问http://localhost:8888/start,以下日志可以说明系统调用了正确的请求处理程序:
การคัดลอกรหัสมีดังนี้:
Server has started.
Request for /start received.
About to route a request for /start
Request handler 'start' was called.
并且在浏览器中打开http://localhost:8888/可以看到这个请求同样被start请求处理程序处理了:
การคัดลอกรหัสมีดังนี้:
Request for / received.
About to route a request for /
Request handler 'start' was called.
让请求处理程序作出响应
ดีมาก.不过现在要是请求处理程序能够向浏览器返回一些有意义的信息而并非全是“Hello World”,那就更好了。
这里要记住的是,浏览器发出请求后获得并显示的“Hello World”信息仍是来自于我们server.js文件中的onRequest函数。
其实“处理请求”说白了就是“对请求作出响应”,因此,我们需要让请求处理程序能够像onRequest函数那样可以和浏览器进行“对话”。
不好的实现方式
对于我们这样拥有PHP或者Ruby技术背景的开发者来说,最直截了当的实现方式事实上并不是非常靠谱: 看似有效,实则未必如此。
这里我指的“直截了当的实现方式”意思是:让请求处理程序通过onRequest函数直接返回(return())他们要展示给用户的信息。
我们先就这样去实现,然后再来看为什么这不是一种很好的实现方式。
让我们从让请求处理程序返回需要在浏览器中显示的信息开始。我们需要将requestHandler.js修改为如下形式:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชั่นเริ่มต้น () {
console.log("Request handler 'start' was called.");
return "Hello Start";
-
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
-
exports.start = start;
exports.upload = upload;
ตกลง同样的,请求路由需要将请求处理程序返回给它的信息返回给服务器。因此,我们需要将router.js修改为如下形式:
การคัดลอกรหัสมีดังนี้:
function route(handle, pathname) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
return handle[pathname]();
} อื่น {
console.log("No request handler found for " + pathname);
return "404 Not found";
-
-
exports.route = route;
正如上述代码所示,当请求无法路由的时候,我们也返回了一些相关的错误信息。
最后,我们需要对我们的server.js进行重构以使得它能够将请求处理程序通过请求路由返回的内容响应给浏览器,如下所示:
การคัดลอกรหัสมีดังนี้:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
response.writeHead(200, {"Content-Type": "text/plain"});
var content = route(handle, pathname)
response.write(content);
response.end();
-
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
-
exports.start = start;
如果我们运行重构后的应用,一切都会工作的很好:请求http://localhost:8888/start,浏览器会输出“Hello Start”,请求http://localhost:8888/upload会输出“Hello Upload”,而请求http://localhost:8888/foo 会输出“404 Not found”。
好,那么问题在哪里呢?简单的说就是: 当未来有请求处理程序需要进行非阻塞的操作的时候,我们的应用就“挂”了。
没理解?没关系,下面就来详细解释下。
阻塞与非阻塞
正如此前所提到的,当在请求处理程序中包括非阻塞操作时就会出问题。但是,在说这之前,我们先来看看什么是阻塞操作。
我不想去解释“阻塞”和“非阻塞”的具体含义,我们直接来看,当在请求处理程序中加入阻塞操作时会发生什么。
这里,我们来修改下start请求处理程序,我们让它等待10秒以后再返回“Hello Start”。因为,JavaScript中没有类似sleep()这样的操作,所以这里只能够来点小Hack来模拟实现。
让我们将requestHandlers.js修改成如下形式:
การคัดลอกรหัสมีดังนี้:
ฟังก์ชั่นเริ่มต้น () {
console.log("Request handler 'start' was called.");
function sleep(milliSeconds) {
var startTime = new Date().getTime();
while (new Date().getTime() < startTime + milliSeconds);
-
sleep(10000);
return "Hello Start";
-
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
-
exports.start = start;
exports.upload = upload;
上述代码中,当函数start()被调用的时候,Node.js会先等待10秒,之后才会返回“Hello Start”。当调用upload()的时候,会和此前一样立即返回。
(当然了,这里只是模拟休眠10秒,实际场景中,这样的阻塞操作有很多,比方说一些长时间的计算操作等。)
接下来就让我们来看看,我们的改动带来了哪些变化。
如往常一样,我们先要重启下服务器。为了看到效果,我们要进行一些相对复杂的操作(跟着我一起做): 首先,打开两个浏览器窗口或者标签页。在第一个浏览器窗口的地址栏中输入http://localhost:8888/start, 但是先不要打开它!
在第二个浏览器窗口的地址栏中输入http://localhost:8888/upload, 同样的,先不要打开它!
接下来,做如下操作:在第一个窗口中(“/start”)按下回车,然后快速切换到第二个窗口中(“/upload”)按下回车。
注意,发生了什么: /start URL加载花了10秒,这和我们预期的一样。但是,/upload URL居然也花了10秒,而它在对应的请求处理程序中并没有类似于sleep()这样的操作!
这到底是为什么呢?原因就是start()包含了阻塞操作。形象的说就是“它阻塞了所有其他的处理工作”。
这显然是个问题,因为Node一向是这样来标榜自己的:“在node中除了代码,所有一切都是并行执行的”。
这句话的意思是说,Node.js可以在不新增额外线程的情况下,依然可以对任务进行并行处理―― Node.js是单线程的。它通过事件轮询(event loop)来实现并行操作,对此,我们应该要充分利用这一点―― 尽可能的避免阻塞操作,取而代之,多使用非阻塞操作。
然而,要用非阻塞操作,我们需要使用回调,通过将函数作为参数传递给其他需要花时间做处理的函数(比方说,休眠10秒,或者查询数据库,又或者是进行大量的计算)。
对于Node.js来说,它是这样处理的:“嘿,probablyExpensiveFunction()(译者注:这里指的就是需要花时间处理的函数),你继续处理你的事情,我(Node.js线程)先不等你了,我继续去处理你后面的代码,请你提供一个callbackFunction(),等你处理完之后我会去调用该回调函数的,谢谢!”
(如果想要了解更多关于事件轮询细节,可以阅读Mixu的博文――理解node.js的事件轮询。)
接下来,我们会介绍一种错误的使用非阻塞操作的方式。
和上次一样,我们通过修改我们的应用来暴露问题。
这次我们还是拿start请求处理程序来“开刀”。将其修改成如下形式:
การคัดลอกรหัสมีดังนี้:
var exec = require("child_process").exec;
ฟังก์ชั่นเริ่มต้น () {
console.log("Request handler 'start' was called.");
var content = "empty";
exec("ls -lah", function (error, stdout, stderr) {
content = stdout;
-
return content;
-
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
-
exports.start = start;
exports.upload = upload;
上述代码中,我们引入了一个新的Node.js模块,child_process。之所以用它,是为了实现一个既简单又实用的非阻塞操作:exec()。
exec()做了什么呢?它从Node.js来执行一个shell命令。在上述例子中,我们用它来获取当前目录下所有的文件(“ls -lah”),然后,当/startURL请求的时候将文件信息输出到浏览器中。
上述代码是非常直观的: 创建了一个新的变量content(初始值为“empty”),执行“ls -lah”命令,将结果赋值给content,最后将content返回。
和往常一样,我们启动服务器,然后访问“http://localhost:8888/start” 。
之后会载入一个漂亮的web页面,其内容为“empty”。怎么回事?
这个时候,你可能大致已经猜到了,exec()在非阻塞这块发挥了神奇的功效。它其实是个很好的东西,有了它,我们可以执行非常耗时的shell操作而无需迫使我们的应用停下来等待该操作。
(如果想要证明这一点,可以将“ls -lah”换成比如“find /”这样更耗时的操作来效果)。
然而,针对浏览器显示的结果来看,我们并不满意我们的非阻塞操作,对吧?
好,接下来,我们来修正这个问题。在这过程中,让我们先来看看为什么当前的这种方式不起作用。
问题就在于,为了进行非阻塞工作,exec()使用了回调函数。
在我们的例子中,该回调函数就是作为第二个参数传递给exec()的匿名函数:
การคัดลอกรหัสมีดังนี้:
function (error, stdout, stderr) {
content = stdout;
-
现在就到了问题根源所在了:我们的代码是同步执行的,这就意味着在调用exec()之后,Node.js会立即执行return content ;在这个时候,content仍然是“empty”,因为传递给exec()的回调函数还未执行到――因为exec()的操作是异步的。
我们这里“ls -lah”的操作其实是非常快的(除非当前目录下有上百万个文件)。这也是为什么回调函数也会很快的执行到―― 不过,不管怎么说它还是异步的。
为了让效果更加明显,我们想象一个更耗时的命令: “find /”,它在我机器上需要执行1分钟左右的时间,然而,尽管在请求处理程序中,我把“ls -lah”换成“find /”,当打开/start URL的时候,依然能够立即获得HTTP响应―― 很明显,当exec()在后台执行的时候,Node.js自身会继续执行后面的代码。并且我们这里假设传递给exec()的回调函数,只会在“find /”命令执行完成之后才会被调用。
那究竟我们要如何才能实现将当前目录下的文件列表显示给用户呢?
好,了解了这种不好的实现方式之后,我们接下来来介绍如何以正确的方式让请求处理程序对浏览器请求作出响应。
以非阻塞操作进行请求响应
我刚刚提到了这样一个短语―― “正确的方式”。而事实上通常“正确的方式”一般都不简单。
不过,用Node.js就有这样一种实现方案: 函数传递。下面就让我们来具体看看如何实现。
So far, our application can pass values between the application layers (request handler->request routing->server) content returned by the request handler (the content that the request handler will eventually display to the user) Pass to the HTTP server .
现在我们采用如下这种新的实现方式:相对采用将内容传递给服务器的方式,我们这次采用将服务器“传递”给内容的方式。 从实践角度来说,就是将response对象(从服务器的回调函数onRequest()获取)通过请求路由传递给请求处理程序。 随后,处理程序就可以采用该对象上的函数来对请求作出响应。
原理就是如此,接下来让我们来一步步实现这种方案。
先从server.js开始:
การคัดลอกรหัสมีดังนี้:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(handle, pathname, response);
-
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
-
exports.start = start;
相对此前从route()函数获取返回值的做法,这次我们将response对象作为第三个参数传递给route()函数,并且,我们将onRequest()处理程序中所有有关response的函数调都移除,因为我们希望这部分工作让route()函数来完成。
下面就来看看我们的router.js:
การคัดลอกรหัสมีดังนี้:
function route(handle, pathname, response) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response);
} อื่น {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
-
-
exports.route = route;
同样的模式:相对此前从请求处理程序中获取返回值,这次取而代之的是直接传递response对象。
如果没有对应的请求处理器处理,我们就直接返回“404”错误。
最后,我们将requestHandler.js修改为如下形式:
การคัดลอกรหัสมีดังนี้:
var exec = require("child_process").exec;
function start(response) {
console.log("Request handler 'start' was called.");
exec("ls -lah", function (error, stdout, stderr) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(stdout);
response.end();
-
-
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
-
exports.start = start;
exports.upload = upload;
我们的处理程序函数需要接收response参数,为了对请求作出直接的响应。
start处理程序在exec()的匿名回调函数中做请求响应的操作,而upload处理程序仍然是简单的回复“Hello World”,只是这次是使用response对象而已。
这时再次我们启动应用(node index.js),一切都会工作的很好。
如果想要证明/start处理程序中耗时的操作不会阻塞对/upload请求作出立即响应的话,可以将requestHandlers.js修改为如下形式:
การคัดลอกรหัสมีดังนี้:
var exec = require("child_process").exec;
function start(response) {
console.log("Request handler 'start' was called.");
exec("find /",
{ timeout: 10000, maxBuffer: 20000*1024 },
function (error, stdout, stderr) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(stdout);
response.end();
-
-
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
-
exports.start = start;
exports.upload = upload;
这样一来,当请求http://localhost:8888/start的时候,会花10秒钟的时间才载入,而当请求http://localhost:8888/upload的时候,会立即响应,纵然这个时候/start响应还在处理中。
更有用的场景
到目前为止,我们做的已经很好了,但是,我们的应用没有实际用途。
服务器,请求路由以及请求处理程序都已经完成了,下面让我们按照此前的用例给网站添加交互:用户选择一个文件,上传该文件,然后在浏览器中看到上传的文件。 为了保持简单,我们假设用户只会上传图片,然后我们应用将该图片显示到浏览器中。
好,下面就一步步来实现,鉴于此前已经对JavaScript原理性技术性的内容做过大量介绍了,这次我们加快点速度。
要实现该功能,分为如下两步: 首先,让我们来看看如何处理POST请求(非文件上传),之后,我们使用Node.js的一个用于文件上传的外部模块。之所以采用这种实现方式有两个理由。
第一,尽管在Node.js中处理基础的POST请求相对比较简单,但在这过程中还是能学到很多。
第二,用Node.js来处理文件上传(multipart POST请求)是比较复杂的,它不在本书的范畴,但,如何使用外部模块却是在本书涉猎内容之内。
处理POST请求
考虑这样一个简单的例子:我们显示一个文本区(textarea)供用户输入内容,然后通过POST请求提交给服务器。最后,服务器接受到请求,通过处理程序将输入的内容展示到浏览器中。
/start请求处理程序用于生成带文本区的表单,因此,我们将requestHandlers.js修改为如下形式:
function start(response) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
-
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
-
exports.start = start;
exports.upload = upload;
好了,现在我们的应用已经很完善了,都可以获得威比奖(Webby Awards)了,哈哈。(译者注:威比奖是由国际数字艺术与科学学院主办的评选全球最佳网站的奖项,具体参见详细说明)通过在浏览器中访问http://localhost:8888/start就可以看到简单的表单了,要记得重启服务器哦!
你可能会说:这种直接将视觉元素放在请求处理程序中的方式太丑陋了。说的没错,但是,我并不想在本书中介绍诸如MVC之类的模式,因为这对于你了解JavaScript或者Node.js环境来说没多大关系。
余下的篇幅,我们来探讨一个更有趣的问题: 当用户提交表单时,触发/upload请求处理程序处理POST请求的问题。
现在,我们已经是新手中的专家了,很自然会想到采用异步回调来实现非阻塞地处理POST请求的数据。
这里采用非阻塞方式处理是明智的,因为POST请求一般都比较“重” ―― 用户可能会输入大量的内容。用阻塞的方式处理大数据量的请求必然会导致用户操作的阻塞。
为了使整个过程非阻塞,Node.js会将POST数据拆分成很多小的数据块,然后通过触发特定的事件,将这些小数据块传递给回调函数。这里的特定的事件有data事件(表示新的小数据块到达了)以及end事件(表示所有的数据都已经接收完毕)。
我们需要告诉Node.js当这些事件触发的时候,回调哪些函数。怎么告诉呢? 我们通过在request对象上注册监听器实现。这里的request对象是每次接收到HTTP请求时候,都会把该对象传递给onRequest回调函数。
ดังที่แสดงด้านล่าง:
การคัดลอกรหัสมีดังนี้:
request.addListener("data", function(chunk) {
// called when a new chunk of data was received
-
request.addListener("end", function() {
// called when all chunks of data have been received
-
问题来了,这部分逻辑写在哪里呢? 我们现在只是在服务器中获取到了request对象―― 我们并没有像之前response对象那样,把request 对象传递给请求路由和请求处理程序。
在我看来,获取所有来自请求的数据,然后将这些数据给应用层处理,应该是HTTP服务器要做的事情。因此,我建议,我们直接在服务器中处理POST数据,然后将最终的数据传递给请求路由和请求处理器,让他们来进行进一步的处理。
因此,实现思路就是: 将data和end事件的回调函数直接放在服务器中,在data事件回调中收集所有的POST数据,当接收到所有数据,触发end事件后,其回调函数调用请求路由,并将数据传递给它,然后,请求路由再将该数据传递给请求处理程序。
还等什么,马上来实现。先从server.js开始:
การคัดลอกรหัสมีดังนี้:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var postData = "";
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
request.setEncoding("utf8");
request.addListener("data", function(postDataChunk) {
postData += postDataChunk;
console.log("Received POST data chunk '"+
postDataChunk + "'.");
-
request.addListener("end", function() {
route(handle, pathname, response, postData);
-
-
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
-
exports.start = start;
上述代码做了三件事情: 首先,我们设置了接收数据的编码格式为UTF-8,然后注册了“data”事件的监听器,用于收集每次接收到的新数据块,并将其赋值给postData 变量,最后,我们将请求路由的调用移到end事件处理程序中,以确保它只会当所有数据接收完毕后才触发,并且只触发一次。我们同时还把POST数据传递给请求路由,因为这些数据,请求处理程序会用到。
上述代码在每个数据块到达的时候输出了日志,这对于最终生产环境来说,是很不好的(数据量可能会很大,还记得吧?),但是,在开发阶段是很有用的,有助于让我们看到发生了什么。
我建议可以尝试下,尝试着去输入一小段文本,以及大段内容,当大段内容的时候,就会发现data事件会触发多次。
再来点酷的。我们接下来在/upload页面,展示用户输入的内容。要实现该功能,我们需要将postData传递给请求处理程序,修改router.js为如下形式:
การคัดลอกรหัสมีดังนี้:
function route(handle, pathname, response, postData) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response, postData);
} อื่น {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
-
-
exports.route = route;
然后,在requestHandlers.js中,我们将数据包含在对upload请求的响应中:
การคัดลอกรหัสมีดังนี้:
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
-
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent: " + postData);
response.end();
-
exports.start = start;
exports.upload = upload;
好了,我们现在可以接收POST数据并在请求处理程序中处理该数据了。
我们最后要做的是: 当前我们是把请求的整个消息体传递给了请求路由和请求处理程序。我们应该只把POST数据中,我们感兴趣的部分传递给请求路由和请求处理程序。在我们这个例子中,我们感兴趣的其实只是text字段。
我们可以使用此前介绍过的querystring模块来实现:
การคัดลอกรหัสมีดังนี้:
var querystring = require("querystring");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
-
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: "+
querystring.parse(postData).text);
response.end();
-
exports.start = start;
exports.upload = upload;
好了,以上就是关于处理POST数据的全部内容。
处理文件上传
最后,我们来实现我们最终的用例:允许用户上传图片,并将该图片在浏览器中显示出来。
回到90年代,这个用例完全可以满足用于IPO的商业模型了,如今,我们通过它能学到这样两件事情: 如何安装外部Node.js模块,以及如何将它们应用到我们的应用中。
这里我们要用到的外部模块是Felix Geisendörfer开发的node-formidable模块。它对解析上传的文件数据做了很好的抽象。 其实说白了,处理文件上传“就是”处理POST数据―― 但是,麻烦的是在具体的处理细节,所以,这里采用现成的方案更合适点。
使用该模块,首先需要安装该模块。Node.js有它自己的包管理器,叫NPM。它可以让安装Node.js的外部模块变得非常方便。通过如下一条命令就可以完成该模块的安装:
การคัดลอกรหัสมีดังนี้:
npm install formidable
如果终端输出如下内容:
การคัดลอกรหัสมีดังนี้:
npm info build Success: [email protected]
npm ok
就说明模块已经安装成功了。
现在我们就可以用formidable模块了――使用外部模块与内部模块类似,用require语句将其引入即可:
การคัดลอกรหัสมีดังนี้:
var formidable = require("formidable");
这里该模块做的就是将通过HTTP POST请求提交的表单,在Node.js中可以被解析。我们要做的就是创建一个新的IncomingForm,它是对提交表单的抽象表示,之后,就可以用它解析request对象,获取表单中需要的数据字段。
node-formidable官方的例子展示了这两部分是如何融合在一起工作的:
การคัดลอกรหัสมีดังนี้:
var formidable = require('formidable'),
http = require('http'),
sys = require('sys');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
// parse a file upload
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:/n/n');
res.end(sys.inspect({fields: fields, files: files}));
-
กลับ;
-
// show a file upload form
res.writeHead(200, {'content-type': 'text/html'});
res.end(
'<form action="/upload" enctype="multipart/form-data" '+
'method="post">'+
'<input type="text" name="title"><br>'+
'<input type="file" name="upload" multiple="multiple"><br>'+
'<input type="submit" value="Upload">'+
'</form>'
-
}).listen(8888);
如果我们将上述代码,保存到一个文件中,并通过node来执行,就可以进行简单的表单提交了,包括文件上传。然后,可以看到通过调用form.parse传递给回调函数的files对象的内容,如下所示:
การคัดลอกรหัสมีดังนี้:
received upload:
{ fields: { title: 'Hello World' },
files:
{ upload:
{ size: 1558,
path: '/tmp/1c747974a27a6292743669e91f29350b',
name: 'us-flag.png',
type: 'image/png',
lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT,
_writeStream: [Object],
length: [Getter],
filename: [Getter],
mime: [Getter] } } }
为了实现我们的功能,我们需要将上述代码应用到我们的应用中,另外,我们还要考虑如何将上传文件的内容(保存在/tmp目录中)显示到浏览器中。
我们先来解决后面那个问题: 对于保存在本地硬盘中的文件,如何才能在浏览器中看到呢?
显然,我们需要将该文件读取到我们的服务器中,使用一个叫fs的模块。
我们来添加/showURL的请求处理程序,该处理程序直接硬编码将文件/tmp/test.png内容展示到浏览器中。当然了,首先需要将该图片保存到这个位置才行。
将requestHandlers.js修改为如下形式:
การคัดลอกรหัสมีดังนี้:
var querystring = require("querystring"),
fs = require("fs");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" '+
'content="text/html; charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
-
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: "+
querystring.parse(postData).text);
response.end();
-
function show(response, postData) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if(error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "/n");
response.end();
} อื่น {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
-
-
-
exports.start = start;
exports.upload = upload;
exports.show = show;
我们还需要将这新的请求处理程序,添加到index.js中的路由映射表中:
การคัดลอกรหัสมีดังนี้:
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
handle["/show"] = requestHandlers.show;
server.start(router.route, handle);
重启服务器之后,通过访问http://localhost:8888/show,就可以看到保存在/tmp/test.png的图片了。
好,最后我们要的就是:
在/start表单中添加一个文件上传元素
将node-formidable整合到我们的upload请求处理程序中,用于将上传的图片保存到/tmp/test.png
将上传的图片内嵌到/uploadURL输出的HTML中
第一项很简单。只需要在HTML表单中,添加一个multipart/form-data的编码类型,移除此前的文本区,添加一个文件上传组件,并将提交按钮的文案改为“Upload file”即可。 如下requestHandler.js所示:
การคัดลอกรหัสมีดังนี้:
var querystring = require("querystring"),
fs = require("fs");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" '+
'content="text/html; charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" enctype="multipart/form-data" '+
'method="post">'+
'<input type="file" name="upload">'+
'<input type="submit" value="Upload file" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
-
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: "+
querystring.parse(postData).text);
response.end();
-
function show(response, postData) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if(error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "/n");
response.end();
} อื่น {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
-
-
-
exports.start = start;
exports.upload = upload;
exports.show = show;
ดีมาก.下一步相对比较复杂。这里有这样一个问题: 我们需要在upload处理程序中对上传的文件进行处理,这样的话,我们就需要将request对象传递给node-formidable的form.parse函数。
但是,我们有的只是response对象和postData数组。看样子,我们只能不得不将request对象从服务器开始一路通过请求路由,再传递给请求处理程序。 或许还有更好的方案,但是,不管怎么说,目前这样做可以满足我们的需求。
到这里,我们可以将postData从服务器以及请求处理程序中移除了―― 一方面,对于我们处理文件上传来说已经不需要了,另外一方面,它甚至可能会引发这样一个问题: 我们已经“消耗”了request对象中的数据,这意味着,对于form.parse来说,当它想要获取数据的时候就什么也获取不到了。(因为Node.js不会对数据做缓存)
我们从server.js开始―― 移除对postData的处理以及request.setEncoding (这部分node-formidable自身会处理),转而采用将request对象传递给请求路由的方式:
การคัดลอกรหัสมีดังนี้:
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(handle, pathname, response, request);
-
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
-
exports.start = start;
接下来是router.js ―― 我们不再需要传递postData了,这次要传递request对象:
function route(handle, pathname, response, request) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response, request);
} อื่น {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/html"});
response.write("404 Not found");
response.end();
-
-
exports.route = route;
现在,request对象就可以在我们的upload请求处理程序中使用了。node-formidable会处理将上传的文件保存到本地/tmp目录中,而我们需要做的是确保该文件保存成/tmp/test.png。 没错,我们保持简单,并假设只允许上传PNG图片。
这里采用fs.renameSync(path1,path2)来实现。要注意的是,正如其名,该方法是同步执行的, 也就是说,如果该重命名的操作很耗时的话会阻塞。 这块我们先不考虑。
接下来,我们把处理文件上传以及重命名的操作放到一起,如下requestHandlers.js所示:
การคัดลอกรหัสมีดังนี้:
var querystring = require("querystring"),
fs = require("fs"),
formidable = require("formidable");
function start(response) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" enctype="multipart/form-data" '+
'method="post">'+
'<input type="file" name="upload" multiple="multiple">'+
'<input type="submit" value="Upload file" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
-
function upload(response, request) {
console.log("Request handler 'upload' was called.");
var form = new formidable.IncomingForm();
console.log("about to parse");
form.parse(request, function(error, fields, files) {
console.log("parsing done");
fs.renameSync(files.upload.path, "/tmp/test.png");
response.writeHead(200, {"Content-Type": "text/html"});
response.write("received image:<br/>");
response.write("<img src='/show' />");
response.end();
-
-
function show(response) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if(error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "/n");
response.end();
} อื่น {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
-
-
-
exports.start = start;
exports.upload = upload;
exports.show = show;
好了,重启服务器,我们应用所有的功能就可以用了。选择一张本地图片,将其上传到服务器,然后浏览器就会显示该图片。
สรุปและโอกาส
恭喜,我们的任务已经完成了!我们开发完了一个Node.js的web应用,应用虽小,但却“五脏俱全”。 期间,我们介绍了很多技术点:服务端JavaScript、函数式编程、阻塞与非阻塞、回调、事件、内部和外部模块等等。
当然了,还有许多本书没有介绍到的: 如何操作数据库、如何进行单元测试、如何开发Node.js的外部模块以及一些简单的诸如如何获取GET请求之类的方法。
但本书毕竟只是一本给初学者的教程―― 不可能覆盖到所有的内容。
幸运的是,Node.js社区非常活跃(作个不恰当的比喻就是犹如一群有多动症小孩子在一起,能不活跃吗?), 这意味着,有许多关于Node.js的资源,有什么问题都可以向社区寻求解答。 其中Node.js社区的wiki以及NodeCloud就是最好的资源。