เริ่มต้นใช้งาน Express และ ES6+ JavaScript Stack

เผยแพร่แล้ว: 2022-03-10
สรุปอย่างรวดเร็ว ↬ บทนำเบื้องต้นเกี่ยวกับกระบวนการพัฒนาเว็บแอปพลิเคชันแบ็กเอนด์ด้วย Express — พูดคุยเกี่ยวกับฟีเจอร์ ES6+ JavaScript ที่รั่วไหล, รูปแบบการออกแบบจากโรงงาน, การดำเนินงาน MongoDB CRUD, เซิร์ฟเวอร์และพอร์ต และอนาคตด้วยรูปแบบสถาปัตยกรรมระดับ n ระดับองค์กรสำหรับโปรเจ็กต์ TypeScript

บทความนี้เป็นส่วนที่สองในซีรีส์ โดยมีส่วนที่หนึ่งอยู่ที่นี่ ซึ่งให้ข้อมูลเชิงลึกพื้นฐานและ (หวังว่า) ที่เข้าใจง่ายใน Node.js, ES6+ JavaScript, ฟังก์ชันการโทรกลับ, ฟังก์ชันลูกศร, APIs, โปรโตคอล HTTP, JSON, MongoDB และ มากกว่า.

ในบทความนี้ เราจะสร้างจากทักษะที่เราได้รับในก่อนหน้านี้ เรียนรู้วิธีปรับใช้และปรับใช้ฐานข้อมูล MongoDB สำหรับการจัดเก็บข้อมูลรายการหนังสือของผู้ใช้ สร้าง API ด้วย Node.js และเฟรมเวิร์ก Express Web Application เพื่อแสดงฐานข้อมูลนั้น และดำเนินการ CRUD Operations กับมัน และอีกมากมาย ระหว่างทาง เราจะพูดถึง ES6 Object Destructuring, ES6 Object Shorthand, Async/Await syntax, Spread Operator และเราจะมาดู CORS, Same Origin Policy และอื่นๆ อย่างคร่าวๆ

ในบทความต่อมา เราจะปรับโครงสร้างโค้ดเบสของเราใหม่เพื่อแยกข้อกังวลโดยใช้สถาปัตยกรรมสามชั้นและบรรลุการผกผันของการควบคุมผ่าน Dependency Injection เราจะดำเนินการ JSON Web Token และ Firebase Authentication ตามการรักษาความปลอดภัยและการควบคุมการเข้าถึง เรียนรู้วิธีอย่างปลอดภัย จัดเก็บรหัสผ่าน และใช้ AWS Simple Storage Service เพื่อจัดเก็บอวาตาร์ของผู้ใช้ด้วย Node.js บัฟเฟอร์และสตรีม ทั้งหมดนี้ใช้ PostgreSQL สำหรับการคงอยู่ของข้อมูล ระหว่างทาง เราจะเขียน codebase ใหม่ตั้งแต่ต้นจนจบใน TypeScript เพื่อตรวจสอบแนวคิด OOP แบบคลาสสิก (เช่น Polymorphism, Inheritance, Composition และอื่นๆ) และแม้แต่รูปแบบการออกแบบ เช่น Factory และ Adapters

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

คำเตือน

มีปัญหากับบทความส่วนใหญ่ที่พูดถึง Node.js ในปัจจุบัน ส่วนใหญ่ ไม่ใช่ทั้งหมด ไม่ได้ไปไกลกว่าการอธิบายวิธีตั้งค่า Express Routing รวม Mongoose และอาจใช้ JSON Web Token Authentication ปัญหาคือพวกเขาไม่ได้พูดถึงสถาปัตยกรรม หรือแนวทางปฏิบัติด้านความปลอดภัยที่ดีที่สุด หรือเกี่ยวกับหลักการเข้ารหัสที่ชัดเจน หรือการปฏิบัติตามข้อกำหนดของ ACID ฐานข้อมูลเชิงสัมพันธ์ รูปแบบปกติที่ห้า ทฤษฎีบท CAP หรือธุรกรรม สันนิษฐานว่าคุณรู้เรื่องทั้งหมดที่เข้ามา หรือคุณจะไม่สร้างโครงการขนาดใหญ่หรือเป็นที่นิยมมากพอที่จะรับประกันความรู้ดังกล่าว

ดูเหมือนว่าจะมีนักพัฒนาโหนดหลายประเภท — ในหมู่คนอื่น ๆ บางคนยังใหม่ต่อการเขียนโปรแกรมโดยทั่วไปและคนอื่น ๆ มาจากประวัติศาสตร์อันยาวนานของการพัฒนาองค์กรด้วย C # และ .NET Framework หรือ Java Spring Framework บทความส่วนใหญ่จัดทำขึ้นเพื่อกลุ่มเดิม

ในบทความนี้ ฉันจะทำสิ่งที่ฉันเพิ่งแจ้งว่ามีบทความมากเกินไป แต่ในบทความต่อ ๆ ไป เราจะจัดโครงสร้างโค้ดเบสใหม่ทั้งหมด อนุญาตให้ฉันอธิบายหลักการต่างๆ เช่น Dependency Injection, Three- สถาปัตยกรรมเลเยอร์ (คอนโทรลเลอร์/บริการ/พื้นที่เก็บข้อมูล) การแมปข้อมูลและบันทึกที่ใช้งาน รูปแบบการออกแบบ หน่วย การผสานรวม และการทดสอบการกลายพันธุ์, หลักการ SOLID, หน่วยการทำงาน, การเข้ารหัสเทียบกับอินเทอร์เฟซ, แนวทางปฏิบัติด้านความปลอดภัยที่ดีที่สุด เช่น HSTS, CSRF, NoSQL และ SQL Injection การป้องกัน เป็นต้น. นอกจากนี้เรายังจะย้ายจาก MongoDB ไปยัง PostgreSQL โดยใช้ตัวสร้างแบบสอบถามอย่างง่าย Knex แทน ORM ซึ่งอนุญาตให้เราสร้างโครงสร้างพื้นฐานการเข้าถึงข้อมูลของเราเองและได้ใกล้ชิดและเป็นส่วนตัวด้วย Structured Query Language ซึ่งเป็นความสัมพันธ์ประเภทต่างๆ (หนึ่ง- ต่อหนึ่ง หลายต่อหลาย ฯลฯ) และอื่นๆ บทความนี้จึงควรดึงดูดผู้เริ่มใช้งาน แต่บทความต่อๆ ไปนี้ควรให้ความสำคัญกับนักพัฒนาระดับกลางมากกว่าที่ต้องการปรับปรุงสถาปัตยกรรมของตน

ในส่วนนี้ เราจะกังวลเกี่ยวกับการคงอยู่ของข้อมูลหนังสือเท่านั้น เราจะไม่จัดการการพิสูจน์ตัวตนผู้ใช้ การแฮชรหัสผ่าน สถาปัตยกรรม หรือสิ่งที่ซับซ้อนเช่นนั้น ทั้งหมดนี้จะมาในบทความหน้าและบทความต่อๆ ไป สำหรับตอนนี้ และโดยพื้นฐานแล้ว เราจะสร้างวิธีการที่จะอนุญาตให้ลูกค้าสื่อสารกับเว็บเซิร์ฟเวอร์ของเราผ่านโปรโตคอล HTTP เพื่อบันทึกข้อมูลหนังสือในฐานข้อมูล

หมายเหตุ : ฉันได้ตั้งใจทำให้มันเรียบง่ายสุดๆ และบางทีอาจไม่ใช่ทั้งหมดที่ใช้ได้จริง เพราะบทความนี้ ในตัวของมันเองนั้นยาวมาก เพราะฉันได้ใช้เสรีภาพในการเบี่ยงเบนเพื่อหารือเกี่ยวกับหัวข้อเพิ่มเติม ดังนั้น เราจะค่อยๆ ปรับปรุงคุณภาพและความซับซ้อนของ API ในซีรีส์นี้ แต่อีกครั้ง เนื่องจากฉันกำลังพิจารณาว่านี่เป็นหนึ่งในการแนะนำ Express ครั้งแรกของคุณ ฉันจึงตั้งใจทำให้สิ่งต่าง ๆ เป็นเรื่องง่ายมาก

  1. การทำลายโครงสร้างวัตถุ ES6
  2. ES6 วัตถุชวเลข
  3. ตัวดำเนินการกระจาย ES6 (...)
  4. ขึ้นมา...

การทำลายโครงสร้างวัตถุ ES6

ES6 Object Destructuring หรือ Destructuring Assignment Syntax เป็นวิธีการแยกหรือแยกค่าออกจากอาร์เรย์หรืออ็อบเจ็กต์ไปยังตัวแปรของพวกมันเอง เราจะเริ่มด้วยคุณสมบัติของอ็อบเจกต์และอภิปรายองค์ประกอบอาร์เรย์

 const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; // Log properties: console.log('Name:', person.name); console.log('Occupation:', person.occupation);

การดำเนินการดังกล่าวค่อนข้างจะดั้งเดิม แต่อาจค่อนข้างยุ่งยากเมื่อพิจารณาว่าเราต้องอ้างอิง person.something ต่อไปบางสิ่งบางอย่างทุกที่ สมมติว่ามีอีก 10 แห่งในโค้ดของเราที่เราต้องทำอย่างนั้น — มันจะค่อนข้างลำบากค่อนข้างเร็ว วิธีการสั้น ๆ คือการกำหนดค่าเหล่านี้ให้กับตัวแปรของตนเอง

 const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; const personName = person.name; const personOccupation = person.occupation; // Log properties: console.log('Name:', personName); console.log('Occupation:', personOccupation);

บางทีนี่อาจดูสมเหตุสมผล แต่ถ้าเรามีทรัพย์สินอื่นอีก 10 รายการซ้อนอยู่บนวัตถุของ person ด้วยล่ะ นั่นอาจเป็นหลายบรรทัดที่ไม่จำเป็นเพียงเพื่อกำหนดค่าให้กับตัวแปร - ณ จุดนั้นเราตกอยู่ในอันตรายเพราะหากคุณสมบัติของวัตถุถูกกลายพันธุ์ ตัวแปรของเราจะไม่สะท้อนถึงการเปลี่ยนแปลงนั้น (จำไว้ว่า เฉพาะการอ้างอิงถึงวัตถุเท่านั้นที่ไม่เปลี่ยนรูปแบบด้วยการกำหนดค่า const ไม่ใช่คุณสมบัติของวัตถุ) ดังนั้นโดยพื้นฐานแล้วเราไม่สามารถรักษา "สถานะ" (และฉันกำลังใช้คำนั้นอย่างหลวม ๆ ) ให้ตรงกัน ผ่านโดยการอ้างอิงเทียบกับค่าผ่านอาจเข้ามาเล่นที่นี่ แต่ฉันไม่ต้องการหลงทางไกลจากขอบเขตของส่วนนี้มากเกินไป

ES6 Object Destructing โดยพื้นฐานแล้วให้เราทำสิ่งนี้:

 const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; // This is new. It's called Object Destructuring. const { name, occupation } = person; // Log properties: console.log('Name:', name); console.log('Occupation:', occupation);

เรา ไม่ได้ สร้างอ็อบเจกต์/อ็อบเจกต์ตามตัวอักษร เรากำลังเปิดแพ็กเกจ name และคุณสมบัติ occupation จากอ็อบเจกต์ดั้งเดิมและใส่ลงในตัวแปรของชื่อเดียวกัน ชื่อที่เราใช้ต้องตรงกับชื่อคุณสมบัติที่เราต้องการแยก

อีกครั้ง ไวยากรณ์ const { a, b } = someObject; โดยเฉพาะอย่างยิ่งบอกว่าเราคาดว่าคุณสมบัติบางอย่าง a และคุณสมบัติบางอย่าง b จะมีอยู่ใน someObject (เช่น someObject อาจเป็น { a: 'dataA', b: 'dataB' } เป็นต้น) และเราต้องการวางค่าใดก็ตาม ของคีย์/คุณสมบัติเหล่านั้นภายในตัวแปร const ที่มีชื่อเดียวกัน นั่นเป็นเหตุผลที่ไวยากรณ์ข้างต้นจะให้ตัวแปรสองตัวแปรกับเราและ const a = someObject.a และ const b = someObject.b

นั่นหมายความว่ามีสองด้านในการทำลายวัตถุ ด้าน "เทมเพลต" และด้าน "แหล่งที่มา" โดยที่ด้าน const { a, b } (ด้านซ้ายมือ) เป็น เทมเพลต และด้าน someObject (ด้านขวา) เป็นฝั่ง ต้นทาง ซึ่งสมเหตุสมผล — เรากำลังกำหนดโครงสร้างหรือ “แม่แบบ” ทางด้านซ้ายที่สะท้อนข้อมูลทางด้าน “ต้นทาง”

อีกครั้ง เพื่อให้ชัดเจน นี่คือตัวอย่างบางส่วน:

 // ----- Destructure from Object Variable with const ----- // const objOne = { a: 'dataA', b: 'dataB' }; // Destructure const { a, b } = objOne; console.log(a); // dataA console.log(b); // dataB // ----- Destructure from Object Variable with let ----- // let objTwo = { c: 'dataC', d: 'dataD' }; // Destructure let { c, d } = objTwo; console.log(c); // dataC console.log(d); // dataD // Destructure from Object Literal with const ----- // const { e, f } = { e: 'dataE', f: 'dataF' }; // <-- Destructure console.log(e); // dataE console.log(f); // dataF // Destructure from Object Literal with let ----- // let { g, h } = { g: 'dataG', h: 'dataH' }; // <-- Destructure console.log(g); // dataG console.log(h); // dataH

ในกรณีของคุณสมบัติที่ซ้อนกัน ให้มิเรอร์โครงสร้างเดียวกันในงานมอบหมายการทำลายของคุณ:

 const person = { name: 'Richard P. Feynman', occupation: { type: 'Theoretical Physicist', location: { lat: 1, lng: 2 } } }; // Attempt one: const { name, occupation } = person; console.log(name); // Richard P. Feynman console.log(occupation); // The entire `occupation` object. // Attempt two: const { occupation: { type, location } } = person; console.log(type); // Theoretical Physicist console.log(location) // The entire `location` object. // Attempt three: const { occupation: { location: { lat, lng } } } = person; console.log(lat); // 1 console.log(lng); // 2

อย่างที่คุณเห็น คุณสมบัติที่คุณตัดสินใจที่จะดึงออกมานั้นเป็นทางเลือก และหากต้องการแกะคุณสมบัติที่ซ้อนกัน เพียงแค่จำลองโครงสร้างของวัตถุดั้งเดิม (แหล่งที่มา) ในด้านเทมเพลตของไวยากรณ์การทำลายล้างของคุณ หากคุณพยายามทำลายโครงสร้างคุณสมบัติที่ไม่มีอยู่บนวัตถุดั้งเดิม ค่านั้นจะไม่ถูกกำหนด

เราสามารถทำลายโครงสร้างตัวแปรเพิ่มเติมโดยไม่ต้องประกาศก่อน — การมอบหมายโดยไม่ต้องประกาศ — โดยใช้ไวยากรณ์ต่อไปนี้:

 let name, occupation; const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; ;({ name, occupation } = person); console.log(name); // Richard P. Feynman console.log(occupation); // Theoretical Physicist

เรานำหน้านิพจน์ด้วยเครื่องหมายอัฒภาคเพื่อให้แน่ใจว่าเราไม่ได้สร้าง IIFE (Immediately Invoked Function Expression) โดยไม่ได้ตั้งใจด้วยฟังก์ชันในบรรทัดก่อนหน้า (หากมีฟังก์ชันดังกล่าวอยู่) และวงเล็บรอบคำสั่งการมอบหมายจำเป็นต้องใช้ หยุด JavaScript ไม่ให้ปฏิบัติกับมือซ้าย (เทมเพลต) ของคุณเป็นบล็อก

มีกรณีการใช้งานทั่วไปในการทำลายโครงสร้างภายในอาร์กิวเมนต์ของฟังก์ชัน:

 const config = { baseUrl: '<baseURL>', awsBucket: '<bucket>', secret: '<secret-key>' // <- Make this an env var. }; // Destructures `baseUrl` and `awsBucket` off `config`. const performOperation = ({ baseUrl, awsBucket }) => { fetch(baseUrl).then(() => console.log('Done')); console.log(awsBucket); // <bucket> }; performOperation(config);

อย่างที่คุณเห็น เราอาจใช้รูปแบบการทำลายโครงสร้างปกติที่เราเคยชินกับฟังก์ชันภายในได้ดังนี้:

 const config = { baseUrl: '<baseURL>', awsBucket: '<bucket>', secret: '<secret-key>' // <- Make this an env var. }; const performOperation = someConfig => { const { baseUrl, awsBucket } = someConfig; fetch(baseUrl).then(() => console.log('Done')); console.log(awsBucket); // <bucket> }; performOperation(config);

แต่การวางไวยากรณ์ดังกล่าวไว้ในลายเซ็นฟังก์ชันจะทำการทำลายโครงสร้างโดยอัตโนมัติและช่วยเราประหยัดบรรทัด

กรณีใช้งานจริงของสิ่งนี้อยู่ใน React Functional Components สำหรับ props :

 import React from 'react'; // Destructure `titleText` and `secondaryText` from `props`. export default ({ titleText, secondaryText }) => ( <div> <h1>{titleText}</h1> <h3>{secondaryText}</h3> </div> );

ตรงข้ามกับ:

 import React from 'react'; export default props => ( <div> <h1>{props.titleText}</h1> <h3>{props.secondaryText}</h3> </div> );

ในทั้งสองกรณี เราสามารถตั้งค่าเริ่มต้นให้กับคุณสมบัติได้เช่นกัน:

 const personOne = { name: 'User One', password: 'BCrypt Hash' }; const personTwo = { password: 'BCrypt Hash' }; const createUser = ({ name = 'Anonymous', password }) => { if (!password) throw new Error('InvalidArgumentException'); console.log(name); console.log(password); return { id: Math.random().toString(36) // <--- Should follow RFC 4122 Spec in real app. .substring(2, 15) + Math.random() .toString(36).substring(2, 15), name: name, // <-- We'll discuss this next. password: password // <-- We'll discuss this next. }; } createUser(personOne); // User One, BCrypt Hash createUser(personTwo); // Anonymous, BCrypt Hash

อย่างที่คุณเห็น ในกรณีที่ name นั้นไม่ปรากฏเมื่อมีการทำลายโครงสร้าง เราจะให้ค่าเริ่มต้นแก่มัน เราสามารถทำได้ด้วยไวยากรณ์ก่อนหน้าเช่นกัน:

 const { a, b, c = 'Default' } = { a: 'dataA', b: 'dataB' }; console.log(a); // dataA console.log(b); // dataB console.log(c); // Default

อาร์เรย์สามารถทำลายโครงสร้างได้เช่นกัน:

 const myArr = [4, 3]; // Destructuring happens here. const [valOne, valTwo] = myArr; console.log(valOne); // 4 console.log(valTwo); // 3 // ----- Destructuring without assignment: ----- // let a, b; // Destructuring happens here. ;([a, b] = [10, 2]); console.log(a + b); // 12

เหตุผลในทางปฏิบัติสำหรับการทำลายอาร์เรย์เกิดขึ้นกับ React Hooks (และมีเหตุผลอื่นอีกมากมาย ฉันแค่ใช้ React เป็นตัวอย่าง)

 import React, { useState } from "react"; export default () => { const [buttonText, setButtonText] = useState("Default"); return ( <button onClick={() => setButtonText("Toggled")}> {buttonText} </button> ); }

ขอให้สังเกต useState กำลังถูกทำลายจากการส่งออก และฟังก์ชัน/ค่าของอาร์เรย์กำลังถูกทำลายจาก hook ของ useState อีกครั้ง ไม่ต้องกังวลหากสิ่งที่กล่าวมาไม่สมเหตุสมผล — คุณต้องเข้าใจ React — และฉันแค่ใช้เป็นตัวอย่างเท่านั้น

แม้ว่าจะมี ES6 Object Destructuring มากกว่านี้ ฉันจะขอพูดถึงอีกหัวข้อหนึ่งที่นี่: Destructuring Renaming ซึ่งมีประโยชน์ในการป้องกันการชนกันของขอบเขตหรือเงาของตัวแปร ฯลฯ สมมติว่าเราต้องการทำลายโครงสร้างคุณสมบัติที่เรียกว่า name จากวัตถุที่เรียกว่า person แต่ มีตัวแปรตาม name ในขอบเขตอยู่แล้ว เราสามารถเปลี่ยนชื่อได้ทันทีด้วยเครื่องหมายทวิภาค:

 // JS Destructuring Naming Collision Example: const name = 'Jamie Corkhill'; const person = { name: 'Alan Turing' }; // Rename `name` from `person` to `personName` after destructuring. const { name: personName } = person; console.log(name); // Jamie Corkhill <-- As expected. console.log(personName); // Alan Turing <-- Variable was renamed.

สุดท้าย เราสามารถตั้งค่าเริ่มต้นด้วยการเปลี่ยนชื่อได้เช่นกัน:

 const name = 'Jamie Corkhill'; const person = { location: 'New York City, United States' }; const { name: personName = 'Anonymous', location } = person; console.log(name); // Jamie Corkhill console.log(personName); // Anonymous console.log(location); // New York City, United States

ดังที่คุณเห็น ในกรณีนี้ name จาก person ( person.name ) จะถูกเปลี่ยนชื่อเป็น personName และตั้งค่าเป็นค่าเริ่มต้นของ Anonymous หากไม่มีอยู่

และแน่นอน สามารถทำได้เช่นเดียวกันในลายเซ็นของฟังก์ชัน:

 const personOne = { name: 'User One', password: 'BCrypt Hash' }; const personTwo = { password: 'BCrypt Hash' }; const createUser = ({ name: personName = 'Anonymous', password }) => { if (!password) throw new Error('InvalidArgumentException'); console.log(personName); console.log(password); return { id: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15), name: personName, password: password // <-- We'll discuss this next. }; } createUser(personOne); // User One, BCrypt Hash createUser(personTwo); // Anonymous, BCrypt Hash

ES6 วัตถุชวเลข

สมมติว่าคุณมีโรงงานดังต่อไปนี้: (เราจะพูดถึงโรงงานในภายหลัง)

 const createPersonFactory = (name, location, position) => ({ name: name, location: location, position: position });

บางคนอาจใช้โรงงานนี้เพื่อสร้างวัตถุ person ดังนี้ นอกจากนี้ โปรดทราบว่าโรงงานกำลังส่งคืนวัตถุโดยปริยาย โดยเห็นได้จากวงเล็บรอบวงเล็บของ Arrow Function

 const person = createPersonFactory('Jamie', 'Texas', 'Developer'); console.log(person); // { ... }

นั่นคือสิ่งที่เรารู้อยู่แล้วจาก ES5 Object Literal Syntax อย่างไรก็ตาม โปรดสังเกตว่าในฟังก์ชันโรงงาน ค่าของคุณสมบัติแต่ละรายการเป็นชื่อเดียวกับตัวระบุคุณสมบัติ (คีย์) เอง นั่นคือ — location: location หรือ name: name ปรากฎว่านั่นเป็นเรื่องปกติที่เกิดขึ้นกับนักพัฒนา JS

ด้วยไวยากรณ์ชวเลขจาก ES6 เราอาจได้ผลลัพธ์เดียวกันโดยการเขียนโรงงานใหม่ดังนี้:

 const createPersonFactory = (name, location, position) => ({ name, location, position }); const person = createPersonFactory('Jamie', 'Texas', 'Developer'); console.log(person);

ผลิตผล:

 { name: 'Jamie', location: 'Texas', position: 'Developer' }

สิ่งสำคัญคือต้องตระหนักว่า เราสามารถใช้ชวเลขนี้ได้เฉพาะเมื่อวัตถุที่เราต้องการสร้างถูกสร้างขึ้นแบบไดนามิกตามตัวแปร โดย ที่ชื่อตัวแปรจะเหมือนกับชื่อของคุณสมบัติที่เราต้องการให้ตัวแปรกำหนด

ไวยากรณ์เดียวกันนี้ใช้ได้กับค่าวัตถุ:

 const createPersonFactory = (name, location, position, extra) => ({ name, location, position, extra // <- right here. }); const extra = { interests: [ 'Mathematics', 'Quantum Mechanics', 'Spacecraft Launch Systems' ], favoriteLanguages: [ 'JavaScript', 'C#' ] }; const person = createPersonFactory('Jamie', 'Texas', 'Developer', extra); console.log(person);

ผลิตผล:

 { name: 'Jamie', location: 'Texas', position: 'Developer', extra: { interests: [ 'Mathematics', 'Quantum Mechanics', 'Spacecraft Launch Systems' ], favoriteLanguages: [ 'JavaScript', 'C#' ] } }

เป็นตัวอย่างสุดท้าย สิ่งนี้ใช้ได้กับตัวอักษรอ็อบเจ็กต์เช่นกัน:

 const id = '314159265358979'; const name = 'Archimedes of Syracuse'; const location = 'Syracuse'; const greatMathematician = { id, name, location };

ตัวดำเนินการสเปรด ES6 (…)

Spread Operator อนุญาตให้เราทำสิ่งต่างๆ ได้หลากหลาย ซึ่งเราจะพูดถึงเรื่องนี้ในบางส่วน

ประการแรก เราสามารถกระจายคุณสมบัติจากวัตถุหนึ่งไปยังวัตถุอื่น:

 const myObjOne = { a: 'a', b: 'b' }; const myObjTwo = { ...myObjOne }:

สิ่งนี้มีผลต่อการวางคุณสมบัติทั้งหมดบน myObjOne ลงบน myObjTwo โดยที่ myObjTwo ตอนนี้ { a: 'a', b: 'b' } เราสามารถใช้วิธีนี้เพื่อแทนที่คุณสมบัติก่อนหน้า สมมติว่าผู้ใช้ต้องการอัปเดตบัญชีของตน:

 const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */ const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */ const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */ const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */

สามารถทำได้เช่นเดียวกันกับอาร์เรย์:

 const apollo13Astronauts = ['Jim', 'Jack', 'Fred']; const apollo11Astronauts = ['Neil', 'Buz', 'Michael']; const unionOfAstronauts = [...apollo13Astronauts, ...apollo11Astronauts]; console.log(unionOfAstronauts); // ['Jim', 'Jack', 'Fred', 'Neil', 'Buz, 'Michael'];

โปรดสังเกตว่าเราสร้างยูเนียนของทั้งสองชุด (อาร์เรย์) โดยกระจายอาร์เรย์ออกเป็นอาร์เรย์ใหม่

ตัวดำเนินการ Rest/Spread ยังมีอะไรอีกมาก แต่มันอยู่นอกเหนือขอบเขตสำหรับบทความนี้ สามารถใช้เพื่อรับอาร์กิวเมนต์หลายตัวในฟังก์ชันได้ ตัวอย่างเช่น หากคุณต้องการเรียนรู้เพิ่มเติม ดูเอกสาร MDN ที่นี่

ES6 ไม่ตรงกัน/รอ

Async/Await เป็นวากยสัมพันธ์ที่บรรเทาความเจ็บปวดจากการโยงสัญญา

คีย์เวิร์ดที่จองไว้จะอนุญาตให้คุณ " await " ข้อตกลงตามสัญญา แต่จะใช้ได้เฉพาะในฟังก์ชันที่มีคีย์เวิร์ด async เท่านั้น สมมติว่าฉันมีฟังก์ชันที่ส่งกลับคำสัญญา ในฟังก์ชัน async ใหม่ ฉันสามารถ await ผลลัพธ์ของคำสัญญานั้นแทนที่จะใช้ .then และ . .catch

 // Returns a promise. const myFunctionThatReturnsAPromise = () => { return new Promise((resolve, reject) => { setTimeout(() => resolve('Hello'), 3000); }); } const myAsyncFunction = async () => { const promiseResolutionResult = await myFunctionThatReturnsAPromise(); console.log(promiseResolutionResult); }; // Writes the log statement after three seconds. myAsyncFunction();

มีบางสิ่งที่ควรทราบที่นี่ เมื่อเราใช้ await ในฟังก์ชัน async เฉพาะค่าที่แก้ไขแล้วเท่านั้นที่จะเข้าสู่ตัวแปรทางด้านซ้าย หากฟังก์ชันปฏิเสธ นั่นคือข้อผิดพลาดที่เราต้องจับ อย่างที่เห็นในอีกสักครู่ นอกจากนี้ ฟังก์ชันใดๆ ที่ทำเครื่องหมาย async จะคืนค่าสัญญาโดยค่าเริ่มต้น

สมมติว่าฉันต้องทำการเรียก API สองครั้ง โดยครั้งแรกมีการตอบกลับ การใช้คำสัญญาและการผูกมัดสัญญา คุณอาจทำเช่นนี้:

 const makeAPICall = route => new Promise((resolve, reject) => { console.log(route) resolve(route); }); const main = () => { makeAPICall('/whatever') .then(response => makeAPICall(response + ' second call')) .then(response => console.log(response + ' logged')) .catch(err => console.error(err)) }; main(); // Result: /* /whatever /whatever second call /whatever second call logged */

สิ่งที่เกิดขึ้นที่นี่คือครั้งแรกที่เราเรียก makeAPICall ส่งผ่านไปยังมัน /whatever ซึ่งได้รับการบันทึกในครั้งแรก สัญญาจะแก้ไขด้วยคุณค่านั้น จากนั้นเราเรียก makeAPICall อีกครั้ง ส่งผ่านไปยังมัน /whatever second call ซึ่งได้รับการบันทึก และอีกครั้ง สัญญาจะแก้ไขด้วยค่าใหม่นั้น สุดท้าย เราใช้ค่าใหม่นั้น /whatever second call คำสัญญาเพิ่งแก้ไข และบันทึกมันเองในบันทึกสุดท้าย ต่อท้ายบันทึกที่ logged ในตอนท้าย หากไม่สมเหตุสมผล คุณควรพิจารณาการผูกมัดสัญญา

การใช้ async / await เราสามารถ refactor ดังต่อไปนี้:

 const main = async () => { const resultOne = await makeAPICall('/whatever'); const resultTwo = await makeAPICall(resultOne + ' second call'); console.log(resultTwo + ' logged'); };

นี่คือสิ่งที่จะเกิดขึ้น ฟังก์ชันทั้งหมดจะหยุดดำเนินการที่คำสั่ง makeAPICall await แรกจะได้รับการแก้ไข เมื่อมีการแก้ไข ค่าที่แก้ไขแล้วจะถูกวางไว้ใน resultOne เมื่อสิ่งนี้เกิดขึ้น ฟังก์ชันจะย้ายไปยังคำสั่ง await คำสั่งที่สอง และหยุดชั่วคราวอีกครั้งที่นั่นในช่วงระยะเวลาของการตกลงตามสัญญา เมื่อคำมั่นสัญญาได้รับการแก้ไข ผลการแก้ปัญหาจะถูกวางไว้ใน resultTwo หากแนวคิดเกี่ยวกับการเรียกใช้ฟังก์ชันฟังดูไม่เข้าท่า อย่าเพิ่งกลัว แนวคิดนี้ยังคงเป็นแบบอะซิงโครนัส และฉันจะพูดถึงสาเหตุในอีกสักครู่

นี่เป็นเพียงภาพเส้นทางที่ "มีความสุข" เท่านั้น ในกรณีที่สัญญาข้อใดข้อหนึ่งถูกปฏิเสธ เราสามารถจับได้ด้วยการลอง/จับ เพราะหากคำสัญญาปฏิเสธ ข้อผิดพลาดก็จะถูกโยนทิ้งไป ซึ่งจะเป็นข้อผิดพลาดตามคำสัญญาที่ถูกปฏิเสธ

 const main = async () => { try { const resultOne = await makeAPICall('/whatever'); const resultTwo = await makeAPICall(resultOne + ' second call'); console.log(resultTwo + ' logged'); } catch (e) { console.log(e) } };

ดังที่ฉันได้กล่าวไว้ก่อนหน้านี้ ฟังก์ชันใด ๆ ที่ประกาศ async จะส่งคืนสัญญา ดังนั้น ถ้าคุณต้องการเรียกใช้ฟังก์ชัน async จากฟังก์ชันอื่น คุณสามารถใช้คำสัญญาปกติ หรือ await ถ้าคุณประกาศฟังก์ชันการเรียก async อย่างไรก็ตาม หากคุณต้องการเรียกใช้ฟังก์ชัน async จากโค้ดระดับบนสุดและรอผลลัพธ์ คุณจะต้องใช้ .then และ . .catch

ตัวอย่างเช่น:

 const returnNumberOne = async () => 1; returnNumberOne().then(value => console.log(value)); // 1

หรือคุณสามารถใช้ Immedieately Invoked Function Expression (IIFE):

 (async () => { const value = await returnNumberOne(); console.log(value); // 1 })();

เมื่อคุณใช้ await ในฟังก์ชัน async การทำงานของฟังก์ชันจะหยุดที่คำสั่ง wait นั้นจนกว่าสัญญาจะตกลง อย่างไรก็ตาม ฟังก์ชันอื่นๆ ทั้งหมดมีอิสระในการดำเนินการ ดังนั้นจึงไม่มีการจัดสรรทรัพยากร CPU เพิ่มเติมหรือเธรดไม่เคยถูกบล็อก ฉันจะพูดอีกครั้ง - การดำเนินการในฟังก์ชันเฉพาะนั้น ณ เวลาที่กำหนดจะหยุดจนกว่าสัญญาจะตกลง แต่ฟังก์ชันอื่น ๆ ทั้งหมดสามารถเริ่มทำงานได้ พิจารณา HTTP Web Server — ตามคำขอ ฟังก์ชันทั้งหมดมีอิสระที่จะเริ่มทำงานสำหรับผู้ใช้ทั้งหมดพร้อมกันเมื่อมีการร้องขอ เพียงว่าไวยากรณ์ async/await จะให้ ภาพลวงตา ว่าการดำเนินการเป็น แบบซิงโครนั สและ บล็อก เพื่อให้ สัญญาว่าจะทำงานได้ง่ายขึ้น แต่ทุกอย่างจะยังคงดีและไม่ตรงกัน

นี่ไม่ใช่ทั้งหมดที่มีให้ async / await แต่ควรช่วยให้คุณเข้าใจหลักการพื้นฐาน

โรงงาน OOP แบบคลาสสิก

ตอนนี้เรากำลังจะออกจากโลก JavaScript และเข้าสู่โลก Java อาจมีบางครั้งที่กระบวนการสร้างอ็อบเจ็กต์ (ในกรณีนี้ อินสแตนซ์ของคลาส — อีกครั้งคือ Java) ค่อนข้างซับซ้อน หรือเมื่อเราต้องการให้อ็อบเจ็กต์ต่างๆ สร้างขึ้นตามชุดของพารามิเตอร์ ตัวอย่างอาจเป็นฟังก์ชันที่สร้างอ็อบเจ็กต์ข้อผิดพลาดต่างๆ โรงงานเป็นรูปแบบการออกแบบทั่วไปในการเขียนโปรแกรมเชิงวัตถุ และโดยพื้นฐานแล้วเป็นฟังก์ชันที่สร้างวัตถุ เพื่อสำรวจสิ่งนี้ ให้เราย้ายออกจาก JavaScript ไปสู่โลกของ Java สิ่งนี้จะสมเหตุสมผลสำหรับนักพัฒนาที่มาจาก OOP แบบคลาสสิก (เช่น ไม่ใช่ต้นแบบ) พื้นหลังภาษาที่พิมพ์แบบสแตติก หากคุณไม่ใช่นักพัฒนาซอฟต์แวร์ดังกล่าว โปรดข้ามส่วนนี้ไปได้เลย นี่เป็นส่วนเบี่ยงเบนเล็กน้อย ดังนั้นหากการปฏิบัติตามนี้ขัดจังหวะการทำงานของ JavaScript ของคุณ โปรดข้ามส่วนนี้อีกครั้ง

รูปแบบการสร้างสรรค์ทั่วไป รูปแบบโรงงานอนุญาตให้เราสร้างวัตถุโดยไม่ต้องเปิดเผยตรรกะทางธุรกิจที่จำเป็นเพื่อดำเนินการสร้างดังกล่าว

สมมติว่าเรากำลังเขียนโปรแกรมที่อนุญาตให้เราเห็นภาพรูปร่างดั้งเดิมในมิติ n ตัวอย่างเช่น ถ้าเราระบุลูกบาศก์ เราจะเห็นลูกบาศก์ 2D (สี่เหลี่ยมจัตุรัส) ลูกบาศก์ 3 มิติ (ลูกบาศก์) และลูกบาศก์ 4 มิติ (a Tesseract หรือ Hypercube) นี่คือวิธีที่จะทำได้เล็กน้อยและยกเว้นส่วนการวาดจริงใน Java

 // Main.java // Defining an interface for the shape (can be used as a base type) interface IShape { void draw(); } // Implementing the interface for 2-dimensions: class TwoDimensions implements IShape { @Override public void draw() { System.out.println("Drawing a shape in 2D."); } } // Implementing the interface for 3-dimensions: class ThreeDimensions implements IShape { @Override public void draw() { System.out.println("Drawing a shape in 3D."); } } // Implementing the interface for 4-dimensions: class FourDimensions implements IShape { @Override public void draw() { System.out.println("Drawing a shape in 4D."); } } // Handles object creation class ShapeFactory { // Factory method (notice return type is the base interface) public IShape createShape(int dimensions) { switch(dimensions) { case 2: return new TwoDimensions(); case 3: return new ThreeDimensions(); case 4: return new FourDimensions(); default: throw new IllegalArgumentException("Invalid dimension."); } } } // Main class and entry point. public class Main { public static void main(String[] args) throws Exception { ShapeFactory shapeFactory = new ShapeFactory(); IShape fourDimensions = shapeFactory.createShape(4); fourDimensions.draw(); // Drawing a shape in 4D. } }

อย่างที่คุณเห็น เรากำหนดอินเทอร์เฟซที่ระบุวิธีการวาดรูปร่าง ด้วยการให้คลาสที่แตกต่างกันใช้อินเทอร์เฟซ เราสามารถรับประกันได้ว่ารูปร่างทั้งหมดสามารถวาดได้ (สำหรับพวกเขาทั้งหมดต้องมีวิธีการ draw ที่เขียนทับได้ตามคำจำกัดความของอินเตอร์เฟส เมื่อพิจารณาว่ารูปร่างนี้วาดต่างกันไปขึ้นอยู่กับมิติข้อมูลที่มีการดู เรากำหนดคลาสตัวช่วยที่ใช้อินเทอร์เฟซเพื่อทำงานที่เน้น GPU ในการจำลองการเรนเดอร์ n มิติ ShapeFactory ทำงานเพื่อสร้างอินสแตนซ์ของคลาสที่ถูกต้อง — เมธอด createShape นั้นเป็นแฟคทอรี และเช่นเดียวกับคำจำกัดความข้างต้น มันคือเมธอดที่ส่งคืนอ็อบเจ็กต์ของคลาส ชนิดส่งคืนของ createShape คืออินเทอร์เฟซ IShape เนื่องจากอินเทอร์เฟซ IShape เป็นประเภทพื้นฐานของรูปร่างทั้งหมด (เนื่องจากมีวิธีการ draw )

ตัวอย่าง Java นี้ค่อนข้างไม่สำคัญ แต่คุณสามารถเห็นได้ง่ายว่ามีประโยชน์เพียงใดในแอปพลิเคชันขนาดใหญ่ ซึ่งการตั้งค่าเพื่อสร้างวัตถุอาจไม่ง่ายนัก ตัวอย่างนี้จะเป็นวิดีโอเกม สมมติว่าผู้ใช้ต้องเอาตัวรอดจากศัตรูที่แตกต่างกัน คลาสนามธรรมและอินเทอร์เฟซอาจใช้เพื่อกำหนดฟังก์ชันหลักที่มีให้สำหรับศัตรูทั้งหมด (และวิธีการที่สามารถแทนที่ได้) อาจใช้รูปแบบการมอบหมาย คลาสเบสเดียวและเพื่อให้การทดสอบ/เยาะเย้ย/DI ง่ายขึ้น) สำหรับวัตถุของศัตรูที่สร้างอินสแตนซ์ในรูปแบบต่างๆ อินเทอร์เฟซจะอนุญาตให้สร้างวัตถุจากโรงงานในขณะที่อาศัยประเภทส่วนต่อประสานทั่วไป สิ่งนี้จะมีความเกี่ยวข้องมากหากศัตรูถูกสร้างขึ้นแบบไดนามิก

อีกตัวอย่างหนึ่งคือฟังก์ชันตัวสร้าง สมมติว่าเราใช้ Delegation Pattern เพื่อให้ class delegate ทำงานให้กับ class อื่น ๆ ที่ให้เกียรติอินเทอร์เฟซ เราสามารถวางเมธอดการ build แบบคงที่ในคลาสเพื่อให้สร้างอินสแตนซ์ของตัวเองได้ (สมมติว่าคุณไม่ได้ใช้ Dependency Injection Container/Framework) แทนที่จะต้องโทรหาผู้ตั้งค่าแต่ละคน คุณสามารถทำสิ่งนี้ได้:

 public class User { private IMessagingService msgService; private String name; private int age; public User(String name, int age, IMessagingService msgService) { this.name = name; this.age = age; this.msgService = msgService; } public static User build(String name, int age) { return new User(name, age, new SomeMessageService()); } }

ฉันจะอธิบายรูปแบบการมอบหมายงานในบทความต่อไป หากคุณไม่คุ้นเคยกับรูปแบบนี้ โดยพื้นฐานแล้ว ผ่านองค์ประกอบและในแง่ของการสร้างแบบจำลองวัตถุจะสร้างความสัมพันธ์แบบ "มี-a" แทนที่จะเป็น "เป็น" ความสัมพันธ์ที่คุณจะได้รับกับมรดก หากคุณมีคลาส Mammal และคลาส Dog และ Dog ขยาย Mammal แล้ว Dog is-a Mammal ในขณะที่ถ้าคุณมีคลาส Bark และคุณเพิ่งส่งอินสแตนซ์ของ Bark ไปที่ตัวสร้าง Dog ดังนั้น Dog ก็มี Bark อย่างที่คุณอาจจินตนาการได้ โดยเฉพาะอย่างยิ่งสิ่งนี้ทำให้การทดสอบหน่วยง่ายขึ้น เนื่องจากคุณสามารถใส่การเยาะเย้ยและยืนยันข้อเท็จจริงเกี่ยวกับแบบจำลองได้ตราบใดที่การเยาะเย้ยสนับสนุนสัญญาอินเทอร์เฟซในสภาพแวดล้อมการทดสอบ

วิธีการจากโรงงาน static "สร้าง" ด้านบนเพียงแค่สร้างวัตถุใหม่ของ User และส่งผ่าน MessageService ที่เป็นรูปธรรม โปรดสังเกตว่าสิ่งนี้เป็นไปตามคำจำกัดความด้านบน — ไม่เปิดเผยตรรกะทางธุรกิจเพื่อสร้างวัตถุของคลาส หรือในกรณีนี้ ไม่เปิดเผยการสร้างบริการส่งข้อความต่อผู้โทรเข้าโรงงาน

อีกครั้ง นี้ไม่จำเป็นว่าคุณจะทำสิ่งต่างๆ ในโลกแห่งความเป็นจริงอย่างไร แต่นำเสนอแนวคิดเกี่ยวกับฟังก์ชัน/วิธีการของโรงงานได้ค่อนข้างดี เราอาจใช้คอนเทนเนอร์ Dependency Injection แทน เป็นต้น ตอนนี้กลับไปที่ JavaScript

เริ่มต้นด้วย Express

Express คือ Web Application Framework สำหรับโหนด (พร้อมใช้งานผ่านโมดูล NPM) ที่อนุญาตให้สร้างเว็บเซิร์ฟเวอร์ HTTP สิ่งสำคัญที่ควรทราบคือ Express ไม่ใช่เฟรมเวิร์กเดียวที่จะทำสิ่งนี้ (มี Koa, Fastify เป็นต้น) และดังที่เห็นในบทความก่อนหน้านี้ Node สามารถทำงานได้โดยไม่มี Express เป็นเอนทิตีแบบสแตนด์อโลน (Express เป็นเพียงโมดูลที่ออกแบบมาสำหรับ Node — Node สามารถทำได้หลายอย่างโดยที่ไม่มีมัน แม้ว่า Express จะเป็นที่นิยมสำหรับเว็บเซิร์ฟเวอร์)

อีกครั้ง ให้ฉันสร้างความแตกต่างที่สำคัญมาก มี การแบ่งขั้วระหว่าง Node/JavaScript และ Express โหนด รันไทม์/สภาพแวดล้อมที่คุณเรียกใช้ JavaScript สามารถทำได้หลายอย่าง — เช่น อนุญาตให้คุณสร้างแอป React Native, แอปเดสก์ท็อป, เครื่องมือบรรทัดคำสั่ง ฯลฯ — Express เป็นเพียงเฟรมเวิร์กที่มีน้ำหนักเบาที่อนุญาตให้คุณใช้ Node/JS เพื่อสร้างเว็บเซิร์ฟเวอร์แทนที่จะจัดการกับเครือข่ายระดับต่ำของ Node และ HTTP API คุณไม่จำเป็นต้องใช้ Express เพื่อสร้างเว็บเซิร์ฟเวอร์

ก่อนที่จะเริ่มส่วนนี้ หากคุณไม่คุ้นเคยกับคำขอ HTTP และ HTTP (GET, POST ฯลฯ) เราขอแนะนำให้คุณอ่านส่วนที่เกี่ยวข้องของบทความเก่าของฉัน ซึ่งมีลิงก์ด้านบนนี้

เมื่อใช้ Express เราจะตั้งค่าเส้นทางต่างๆ ที่อาจส่งคำขอ HTTP รวมถึงปลายทางที่เกี่ยวข้อง (ซึ่งเป็นฟังก์ชันเรียกกลับ) ที่จะเริ่มทำงานเมื่อมีการร้องขอไปยังเส้นทางนั้น ไม่ต้องกังวลหากเส้นทางและปลายทางไม่มีความสำคัญ เราจะอธิบายในภายหลัง

ต่างจากบทความอื่นๆ ฉันจะใช้แนวทางในการเขียนซอร์สโค้ดในขณะที่เราไป ทีละบรรทัด แทนที่จะทิ้ง codebase ทั้งหมดไว้ในตัวอย่างเดียวแล้วอธิบายในภายหลัง เริ่มต้นด้วยการเปิดเทอร์มินัล (ฉันใช้ Terminus ที่ด้านบนของ Git Bash บน Windows — ซึ่งเป็นตัวเลือกที่ดีสำหรับผู้ใช้ Windows ที่ต้องการ Bash Shell โดยไม่ต้องตั้งค่าระบบย่อย Linux) ตั้งค่าสำเร็จรูปของโปรเจ็กต์ของเรา และเปิดมันขึ้นมา ในรหัส Visual Studio

 mkdir server && cd server touch server.js npm init -y npm install express code .

ภายในไฟล์ server.js ฉันจะเริ่มต้นด้วยการกำหนดให้ express โดยใช้ฟังก์ชัน require()

 const express = require('express');

require('express') บอกให้ Node ออกไปและรับโมดูล Express ที่เราติดตั้งไว้ก่อนหน้านี้ ซึ่งขณะนี้อยู่ในโฟลเดอร์ node_modules (สำหรับนั่นคือสิ่งที่ npm install ทำ — สร้างโฟลเดอร์ node_modules และวางโมดูลและการพึ่งพาในนั้น) ตามแบบแผนและเมื่อจัดการกับ Express เราเรียกตัวแปรที่เก็บผลลัพธ์ที่ส่งคืนจาก require('express') express แม้ว่ามันอาจจะเรียกว่าอะไรก็ได้ก็ตาม

This returned result, which we have called express , is actually a function — a function we'll have to invoke to create our Express app and set up our routes. Again, by convention, we call this appapp being the return result of express() — that is, the return result of calling the function that has the name express as express() .

 const express = require('express'); const app = express(); // Note that the above variable names are the convention, but not required. // An example such as that below could also be used. const foo = require('express'); const bar = foo(); // Note also that the node module we installed is called express.

The line const app = express(); simply puts a new Express Application inside of the app variable. It calls a function named express (the return result of require('express') ) and stores its return result in a constant named app . If you come from an object-oriented programming background, consider this equivalent to instantiating a new object of a class, where app would be the object and where express() would call the constructor function of the express class. Remember, JavaScript allows us to store functions in variables — functions are first-class citizens. The express variable, then, is nothing more than a mere function. It's provided to us by the developers of Express.

I apologize in advance if I'm taking a very long time to discuss what is actually very basic, but the above, although primitive, confused me quite a lot when I was first learning back-end development with Node.

Inside the Express source code, which is open-source on GitHub, the variable we called express is a function entitled createApplication , which, when invoked, performs the work necessary to create an Express Application:

A snippet of Express source code:

 exports = module.exports = createApplication; /* * Create an express application */ // This is the function we are storing in the express variable. (- Jamie) function createApplication() { // This is what I mean by "Express App" (- Jamie) var app = function(req, res, next) { app.handle(req, res, next); }; mixin(app, EventEmitter.prototype, false); mixin(app, proto, false); // expose the prototype that will get set on requests app.request = Object.create(req, { app: { configurable: true, enumerable: true, writable: true, value: app } }) // expose the prototype that will get set on responses app.response = Object.create(res, { app: { configurable: true, enumerable: true, writable: true, value: app } }) app.init(); // See - `app` gets returned. (- Jamie) return app; }

GitHub: https://github.com/expressjs/express/blob/master/lib/express.js

With that short deviation complete, let's continue setting up Express. Thus far, we have required the module and set up our app variable.

 const express = require('express'); const app = express();

From here, we have to tell Express to listen on a port. Any HTTP Requests made to the URL and Port upon which our application is listening will be handled by Express. We do that by calling app.listen(...) , passing to it the port and a callback function which gets called when the server starts running:

 const PORT = 3000; app.listen(PORT, () => console.log(`Server is up on port {PORT}.`));

We notate the PORT variable in capital by convention, for it is a constant variable that will never change. You could do that with all variables that you declare const , but that would look messy. It's up to the developer or development team to decide on notation, so we'll use the above sparsely. I use const everywhere as a method of “defensive coding” — that is, if I know that a variable is never going to change then I might as well just declare it const . Since I define everything const , I make the distinction between what variables should remain the same on a per-request basis and what variables are true actual global constants.

Here is what we have thus far:

 const express = require('express'); const app = express(); const PORT = 3000; // We will build our API here. // ... // Binding our application to port 3000. app.listen(PORT, () => { console.log(`Server is up on port ${PORT}.`); });

Let's test this to see if the server starts running on port 3000.

I'll open a terminal and navigate to our project's root directory. I'll then run node server/server.js . Note that this assumes you have Node already installed on your system (You can check with node -v ).

If everything works, you should see the following in the terminal:

Server is up on port 3000.

Go ahead and hit Ctrl + C to bring the server back down.

If this doesn't work for you, or if you see an error such as EADDRINUSE , then it means you may have a service already running on port 3000. Pick another port number, like 3001, 3002, 5000, 8000, etc. Be aware, lower number ports are reserved and there is an upper bound of 65535.

At this point, it's worth taking another small deviation as to understand servers and ports in the context of computer networking. We'll return to Express in a moment. I take this approach, rather than introducing servers and ports first, for the purpose of relevance. That is, it is difficult to learn a concept if you fail to see its applicability. In this way, you are already aware of the use case for ports and servers with Express, so the learning experience will be more pleasurable.

A Brief Look At Servers And Ports

A server is simply a computer or computer program that provides some sort of “functionality” to the clients that talk to it. More generally, it's a device, usually connected to the Internet, that handles connections in a pre-defined manner. In our case, that “pre-defined manner” will be HTTP or the HyperText Transfer Protocol. Servers that use the HTTP Protocol are called Web Servers.

When building an application, the server is a critical component of the “client-server model”, for it permits the sharing and syncing of data (generally via databases or file systems) across devices. It's a cross-platform approach, in a way, for the SDKs of platforms against which you may want to code — be they web, mobile, or desktop — all provide methods (APIs) to interact with a server over HTTP or TCP/UDP Sockets. It's important to make a distinction here — by APIs, I mean programming language constructs to talk to a server, like XMLHttpRequest or the Fetch API in JavaScript, or HttpUrlConnection in Java, or even HttpClient in C#/.NET. This is different from the kind of REST API we'll be building in this article to perform CRUD Operations on a database.

To talk about ports, it's important to understand how clients connect to a server. A client requires the IP Address of the server and the Port Number of our specific service on that server. An IP Address, or Internet Protocol Address, is just an address that uniquely identifies a device on a network. Public and private IPs exist, with private addresses commonly used behind a router or Network Address Translator on a local network. You might see private IP Addresses of the form 192.168.XXX.XXX or 10.0.XXX.XXX . When articulating an IP Address, decimals are called “dots”. So 192.168.0.1 (a common router IP Addr.) might be pronounced, “one nine two dot one six eight dot zero dot one”. (By the way, if you're ever in a hotel and your phone/laptop won't direct you to the AP captive portal, try typing 192.168.0.1 or 192.168.1.1 or similar directly into Chrome).

For simplicity, and since this is not an article about the complexities of computer networking, assume that an IP Address is equivalent to a house address, allowing you to uniquely identify a house (where a house is analogous to a server, client, or network device) in a neighborhood. One neighborhood is one network. Put together all of the neighborhoods in the United States, and you have the public Internet. (This is a basic view, and there are many more complexities — firewalls, NATs, ISP Tiers (Tier One, Tier Two, and Tier Three), fiber optics and fiber optic backbones, packet switches, hops, hubs, etc., subnet masks, etc., to name just a few — in the real networking world.) The traceroute Unix command can provide more insight into the above, displaying the path (and associated latency) that packets take through a network as a series of “hops”.

หมายเลขพอร์ตระบุบริการเฉพาะที่ทำงานบนเซิร์ฟเวอร์ SSH หรือ Secure Shell ซึ่งอนุญาตการเข้าถึงเชลล์ระยะไกลไปยังอุปกรณ์ มักทำงานบนพอร์ต 22 FTP หรือ File Transfer Protocol (ซึ่งอาจใช้ตัวอย่างเช่น กับไคลเอ็นต์ FTP เพื่อโอนสินทรัพย์แบบคงที่ไปยังเซิร์ฟเวอร์) โดยทั่วไปจะทำงานบน พอร์ต 21. เราอาจกล่าวได้ว่าท่าเรือเป็นห้องเฉพาะภายในแต่ละบ้านในการเปรียบเทียบข้างต้น สำหรับห้องในบ้านถูกสร้างขึ้นสำหรับสิ่งต่าง ๆ - ห้องนอนสำหรับนอน ห้องครัวสำหรับเตรียมอาหาร ห้องรับประทานอาหารสำหรับการบริโภคดังกล่าว อาหาร ฯลฯ เช่นเดียวกับพอร์ตที่สอดคล้องกับโปรแกรมที่ให้บริการเฉพาะ สำหรับเรา เว็บเซิร์ฟเวอร์มักทำงานบนพอร์ต 80 แม้ว่าคุณจะมีอิสระที่จะระบุหมายเลขพอร์ตใดที่คุณต้องการตราบเท่าที่ไม่ได้ใช้งานโดยบริการอื่น (ไม่สามารถชนกันได้)

ในการเข้าถึงเว็บไซต์ คุณต้องมีที่อยู่ IP ของเว็บไซต์ อย่างไรก็ตาม โดยปกติแล้ว เราเข้าถึงเว็บไซต์ผ่าน URL เบื้องหลัง DNS หรือเซิร์ฟเวอร์ชื่อโดเมนจะแปลง URL นั้นเป็นที่อยู่ IP ทำให้เบราว์เซอร์สามารถสร้างคำขอ GET ไปยังเซิร์ฟเวอร์ รับ HTML และแสดงผลไปยังหน้าจอ 8.8.8.8 คือที่อยู่ของหนึ่งในเซิร์ฟเวอร์ DNS สาธารณะของ Google คุณอาจจินตนาการว่าการต้องการความละเอียดของชื่อโฮสต์เป็นที่อยู่ IP ผ่านเซิร์ฟเวอร์ DNS ระยะไกลนั้นต้องใช้เวลา และคุณก็คิดถูก เพื่อลดความหน่วงแฝง ระบบปฏิบัติการมีแคช DNS ซึ่งเป็นฐานข้อมูลชั่วคราวที่เก็บข้อมูลการค้นหา DNS ซึ่งจะช่วยลดความถี่ในการค้นหาดังกล่าว แคชตัวแก้ไข DNS สามารถดูได้บน Windows ด้วยคำสั่ง ipconfig /displaydns CMD และล้างข้อมูลโดยใช้คำสั่ง ipconfig /flushdns

บนเซิร์ฟเวอร์ Unix พอร์ตหมายเลขล่างทั่วไปเช่น 80 ต้องการสิทธิ์ระดับ รู ท ( เพิ่มขึ้น หากคุณมาจากพื้นหลังของ Windows) ด้วยเหตุผลดังกล่าว เราจะใช้พอร์ต 3000 สำหรับงานพัฒนาของเรา แต่จะอนุญาตให้เซิร์ฟเวอร์เลือกหมายเลขพอร์ต (อะไรก็ได้ที่มี) เมื่อเราปรับใช้กับสภาพแวดล้อมการผลิตของเรา

สุดท้าย โปรดทราบว่าเราสามารถพิมพ์ที่อยู่ IP ได้โดยตรงในแถบค้นหาของ Google Chrome ดังนั้นจึงข้ามกลไกการแก้ปัญหา DNS ตัวอย่างเช่น การพิมพ์ 216.58.194.36 จะนำคุณไปที่ Google.com ในสภาพแวดล้อมการพัฒนาของเรา เมื่อใช้คอมพิวเตอร์ของเราเองเป็นเซิร์ฟเวอร์ dev เราจะใช้ localhost และ port 3000 ที่อยู่ถูกจัดรูปแบบเป็น hostname:port ดังนั้นเซิร์ฟเวอร์ของเราจะใช้ localhost:3000 Localhost หรือ 127.0.0.1 เป็นที่อยู่ลูปแบ็คและหมายถึงที่อยู่ของ "คอมพิวเตอร์เครื่องนี้" เป็นชื่อโฮสต์ และที่อยู่ IPv4 ของมันถูกแก้ไขเป็น 127.0.0.1 ลอง ping localhost บนเครื่องของคุณตอนนี้ คุณอาจได้รับ ::1 back — ซึ่งเป็นที่อยู่ลูปแบ็ค IPv6 หรือ 127.0.0.1 กลับมา — ซึ่งเป็นที่อยู่ลูปแบ็ค IPv4 IPv4 และ IPv6 เป็นรูปแบบที่อยู่ IP ที่แตกต่างกันสองรูปแบบซึ่งเชื่อมโยงกับมาตรฐานที่แตกต่างกัน — ที่อยู่ IPv6 บางรายการสามารถแปลงเป็น IPv4 ได้ แต่ไม่ใช่ทั้งหมด

กลับไปที่ Express

ฉันได้กล่าวถึง HTTP Requests, Verbs และ Status Codes ในบทความก่อนหน้าของฉัน เริ่มต้นใช้งาน Node: An Introduction To APIs, HTTP และ ES6+ JavaScript หากคุณไม่มีความเข้าใจทั่วไปเกี่ยวกับโปรโตคอล โปรดข้ามไปที่ส่วน "คำขอ HTTP และ HTTP" ของส่วนนั้น

เพื่อให้เข้าใจถึง Express เราเพียงแค่ตั้งค่าปลายทางของเราสำหรับการดำเนินการพื้นฐานสี่ประการที่เราจะดำเนินการบนฐานข้อมูล — สร้าง อ่าน อัปเดต และลบ ซึ่งเรียกรวมกันว่า CRUD

โปรดจำไว้ว่า เราเข้าถึงปลายทางตามเส้นทางใน URL กล่าวคือ แม้ว่าโดยทั่วไปจะใช้คำว่า "เส้นทาง" และ "ปลายทาง" สลับกันได้ แต่ ปลายทาง ก็คือฟังก์ชันภาษาโปรแกรมในทางเทคนิค (เช่น ES6 Arrow Functions) ที่ทำการดำเนินการฝั่งเซิร์ฟเวอร์บางส่วน ในขณะที่ เส้นทาง คือสิ่งที่ปลายทางอยู่ ด้านหลัง ของ เราระบุตำแหน่งข้อมูลเหล่านี้เป็นฟังก์ชันเรียกกลับ ซึ่ง Express จะเริ่มทำงานเมื่อมีการร้องขอที่เหมาะสมจากไคลเอนต์ไปยัง เส้นทาง ที่ปลายทางนั้นใช้งานอยู่ คุณสามารถจำข้อมูลข้างต้นได้โดยตระหนักว่าปลายทางนั้นทำหน้าที่หนึ่ง และเส้นทางคือชื่อที่ใช้ในการเข้าถึงจุดปลาย ดังที่เราเห็น เส้นทางเดียวกันสามารถเชื่อมโยงกับจุดปลายหลายจุดโดยใช้ HTTP Verbs ที่แตกต่างกัน (คล้ายกับวิธีการโอเวอร์โหลด ถ้าคุณมาจากพื้นหลัง OOP แบบคลาสสิกที่มี Polymorphism)

โปรดทราบว่าเรากำลังปฏิบัติตามสถาปัตยกรรม REST (REpresentational State Transfer) โดยอนุญาตให้ลูกค้าส่งคำขอไปยังเซิร์ฟเวอร์ของเรา ท้ายที่สุดนี่คือ REST หรือ RESTful API คำขอ เฉพาะที่ส่งไปยัง เส้นทาง เฉพาะจะเริ่มต้น อุปกรณ์ปลายทาง เฉพาะซึ่งจะทำ สิ่งที่ เฉพาะเจาะจง ตัวอย่างของ "สิ่ง" ที่ปลายทางอาจทำคือการเพิ่มข้อมูลใหม่ลงในฐานข้อมูล การลบข้อมูล การอัปเดตข้อมูล ฯลฯ

Express รู้ว่าปลายทางใดที่จะเริ่มทำงาน เพราะเราบอกมันอย่างชัดแจ้ง วิธีการร้องขอ (GET, POST เป็นต้น) และเส้นทาง — เรากำหนดว่าฟังก์ชันใดที่จะเริ่มทำงานสำหรับชุดค่าผสมเฉพาะข้างต้น และไคลเอนต์ส่งคำขอโดยระบุ เส้นทางและวิธีการ เพื่อให้ง่ายขึ้น ด้วย Node เราจะบอก Express ว่า "เฮ้ ถ้ามีคนส่งคำขอ GET ไปยังเส้นทางนี้ ให้ดำเนินการฟังก์ชันนี้ (ใช้จุดปลายนี้)" สิ่งต่างๆ อาจซับซ้อนมากขึ้น: “ด่วน ถ้ามีคนส่งคำขอ GET ไปยังเส้นทาง นี้ แต่พวกเขาไม่ได้ส่ง Authorization Bearer Token ที่ถูกต้องในส่วนหัวของคำขอ ดังนั้นโปรดตอบกลับด้วย HTTP 401 Unauthorized หากพวกเขามี Bearer Token ที่ถูกต้อง โปรดส่งทรัพยากรที่ได้รับการป้องกันที่พวกเขากำลังมองหาโดยการยิงปลายทาง ขอบคุณมากและมีวันที่ดี” อันที่จริง คงจะดีถ้าภาษาโปรแกรมสามารถอยู่ในระดับสูงได้โดยปราศจากความกำกวมรั่วไหล แต่ก็ยังแสดงให้เห็นถึงแนวคิดพื้นฐาน

จำไว้ว่าจุดปลายนั้น อยู่เบื้องหลัง เส้นทาง ดังนั้นจึงจำเป็นที่ไคลเอนต์จะต้องจัดเตรียมวิธีการใดในส่วนหัวของคำขอ เพื่อให้ Express สามารถคิดออกว่าต้องทำอย่างไร คำขอจะทำไปยังเส้นทางเฉพาะซึ่งลูกค้าจะระบุ (พร้อมกับประเภทคำขอ) เมื่อติดต่อกับเซิร์ฟเวอร์ทำให้ Express ทำในสิ่งที่จำเป็นต้องทำและเราทำในสิ่งที่ต้องทำเมื่อ Express เรียกใช้การเรียกกลับของเรา . นั่นคือสิ่งที่เกิดขึ้นทั้งหมด

ในตัวอย่างโค้ดก่อนหน้านี้ เราเรียกฟังก์ชัน listen ซึ่งมีอยู่ใน app โดยส่งผ่านพอร์ตและการโทรกลับ ถ้าคุณจำได้ app เองคือผลลัพธ์จากการเรียกตัวแปร express เป็นฟังก์ชัน (นั่นคือ express() ) และตัวแปร express คือสิ่งที่เราตั้งชื่อผลลัพธ์ที่ส่งคืนจากการกำหนดให้ 'express' จากโฟลเดอร์ node_modules ของเรา เช่นเดียวกับการเรียก listen บน app พ เราระบุ HTTP Request Endpoints โดยการเรียกใช้บน app พ มาดูที่ GET:

 app.get('/my-test-route', () => { // ... });

พารามิเตอร์แรกคือ string และเป็นเส้นทางที่ปลายทางจะทำงาน ฟังก์ชันเรียกกลับเป็นจุดสิ้นสุด ฉันจะพูดอีกครั้ง: ฟังก์ชันเรียกกลับ — พารามิเตอร์ที่สอง — เป็นจุดสิ้นสุด ที่จะเริ่มทำงานเมื่อมีการส่งคำขอ HTTP GET ไปยังเส้นทางใดก็ตามที่เราระบุเป็นอาร์กิวเมนต์แรก ( /my-test-route ในกรณีนี้)

ตอนนี้ ก่อนที่เราจะทำงานกับ Express อีกต่อไป เราจำเป็นต้องรู้ว่าเส้นทางทำงานอย่างไร เส้นทางที่เราระบุเป็นสตริงจะถูกเรียกโดยส่งคำขอไปที่ www.domain.com/the-route-we-chose-earlier-as-a-string ในกรณีของเรา โดเมนคือ localhost:3000 ซึ่งหมายความว่าเพื่อเริ่มการทำงานของฟังก์ชันเรียกกลับด้านบน เราต้องทำ GET Request to localhost:3000/my-test-route หากเราใช้สตริงอื่นเป็นอาร์กิวเมนต์แรกข้างต้น URL จะต้องแตกต่างออกไปเพื่อให้ตรงกับที่เราระบุไว้ใน JavaScript

เมื่อพูดถึงเรื่องเหล่านี้ คุณอาจเคยได้ยิน Glob Patterns เราสามารถพูดได้ว่าเส้นทางของ API ทั้งหมดของเราอยู่ที่ localhost:3000/** Glob Pattern โดยที่ ** เป็น wildcard หมายถึงไดเร็กทอรีหรือไดเร็กทอรีย่อยใดๆ (โปรดทราบว่าเส้นทาง ไม่ใช่ ไดเร็กทอรี) ที่รูทเป็นพาเรนต์ — นั่นคือทุกอย่าง

ไปข้างหน้าและเพิ่มคำสั่งบันทึกลงในฟังก์ชันการโทรกลับนั้นเพื่อให้เรามี:

 // Getting the module from node_modules. const express = require('express'); // Creating our Express Application. const app = express(); // Defining the port we'll bind to. const PORT = 3000; // Defining a new endpoint behind the "/my-test-route" route. app.get('/my-test-route', () => { console.log('A GET Request was made to /my-test-route.'); }); // Binding the server to port 3000. app.listen(PORT, () => { console.log(`Server is up on port ${PORT}.`) });

เราจะทำให้เซิร์ฟเวอร์ของเราทำงานได้โดยเรียกใช้งาน node server/server.js (โดยที่ Node ติดตั้งอยู่ในระบบของเราและสามารถเข้าถึงได้จากทั่วโลกจากตัวแปรสภาพแวดล้อมของระบบ) ในไดเร็กทอรีรากของโปรเจ็กต์ เช่นเดียวกับก่อนหน้านี้ คุณควรเห็นข้อความว่าเซิร์ฟเวอร์อยู่ในคอนโซล ขณะนี้เซิร์ฟเวอร์กำลังทำงาน ให้เปิดเบราว์เซอร์ และไปที่ localhost:3000 ในแถบ URL

คุณควรได้รับข้อความแสดงข้อผิดพลาดที่ระบุว่า Cannot GET / กด Ctrl + Shift + I บน Windows ใน Chrome เพื่อดูคอนโซลนักพัฒนาซอฟต์แวร์ ในนั้น คุณจะเห็นว่าเรามี 404 (ไม่พบทรัพยากร) สมเหตุสมผล — เราบอกเซิร์ฟเวอร์ว่าต้องทำอย่างไรเมื่อมีผู้เยี่ยมชม localhost:3000/my-test-route เท่านั้น เบราว์เซอร์ไม่มีอะไรจะแสดงผลที่ localhost:3000 (ซึ่งเทียบเท่ากับ localhost:3000/ ด้วยเครื่องหมายทับ)

หากคุณดูที่หน้าต่างเทอร์มินัลที่เซิร์ฟเวอร์ทำงานอยู่ ไม่ควรมีข้อมูลใหม่ ตอนนี้ ไปที่ localhost:3000/my-test-route ในแถบ URL ของเบราว์เซอร์ คุณ อาจ เห็นข้อผิดพลาดเดียวกันนี้ในคอนโซลของ Chrome (เนื่องจากเบราว์เซอร์แคชเนื้อหาและยังไม่มี HTML ที่จะแสดง) แต่ถ้าคุณดูเทอร์มินัลของคุณที่กระบวนการของเซิร์ฟเวอร์ทำงานอยู่ คุณจะพบว่าฟังก์ชันการโทรกลับเริ่มทำงานจริง และข้อความบันทึกก็ถูกบันทึกจริงๆ

ปิดเซิร์ฟเวอร์ด้วย Ctrl + C

ตอนนี้ ให้เบราว์เซอร์แสดงผลบางอย่างเมื่อมีการส่งคำขอ GET ไปยังเส้นทางนั้น เพื่อให้เราสูญเสียข้อความ Cannot GET / ข้อความ ฉันจะใช้ app.get() จากก่อนหน้านี้ และในฟังก์ชันเรียกกลับ ฉันจะเพิ่มสองอาร์กิวเมนต์ โปรดจำไว้ว่า ฟังก์ชันเรียกกลับที่เรากำลังส่งผ่านนั้นถูกเรียกโดย Express เบื้องหลัง และ Express สามารถเพิ่มอาร์กิวเมนต์ใดก็ได้ตามต้องการ อันที่จริงมันเพิ่มสองอัน (ในทางเทคนิคแล้วสามอัน แต่เราจะเห็นในภายหลัง) และในขณะที่ทั้งคู่มีความสำคัญอย่างยิ่ง เราไม่ได้สนใจอันแรกในตอนนี้ อาร์กิวเมนต์ที่สองเรียกว่า res ย่อมาจาก response และฉันจะเข้าถึงได้โดยการตั้งค่า undefined เป็นพารามิเตอร์แรก:

 app.get('/my-test-route', (undefined, res) => { console.log('A GET Request was made to /my-test-route.'); });

อีกครั้ง เราสามารถเรียกอาร์กิวเมนต์ res อะไรก็ได้ที่เราต้องการ แต่ res เป็นแบบแผนเมื่อต้องจัดการกับ Express res เป็นวัตถุจริง ๆ และมีวิธีการที่แตกต่างกันในการส่งข้อมูลกลับไปยังไคลเอนต์ ในกรณีนี้ ฉันจะเข้าถึงฟังก์ชัน send(...) ที่มีอยู่ใน res เพื่อส่ง HTML กลับที่เบราว์เซอร์จะแสดงผล อย่างไรก็ตาม เราไม่ได้จำกัดแค่การส่ง HTML กลับ และสามารถเลือกที่จะส่งข้อความกลับ, JavaScript Object, สตรีม (สตรีมนั้นสวยงามเป็นพิเศษ) หรืออะไรก็ตาม

 app.get('/my-test-route', (undefined, res) => { console.log('A GET Request was made to /my-test-route.'); res.send('<h1>Hello, World!</h1>'); });

หากคุณปิดเซิร์ฟเวอร์แล้วเปิดขึ้นมาใหม่ จากนั้นรีเฟรชเบราว์เซอร์ของคุณที่เส้นทาง /my-test-route คุณจะเห็นว่า HTML ได้รับการแสดงผล

แท็บเครือข่ายของเครื่องมือสำหรับนักพัฒนา Chrome จะช่วยให้คุณเห็นคำขอ GET นี้พร้อมรายละเอียดเพิ่มเติมเกี่ยวกับส่วนหัว

ณ จุดนี้ จะเป็นประโยชน์สำหรับเราในการเริ่มเรียนรู้เกี่ยวกับ Express Middleware ซึ่งเป็นฟังก์ชันที่สามารถเริ่มทำงานได้ทั่วโลกหลังจากที่ไคลเอ็นต์ส่งคำขอ

มิดเดิลแวร์ด่วน

Express มีวิธีการกำหนดมิดเดิลแวร์แบบกำหนดเองสำหรับแอปพลิเคชันของคุณ แท้จริงแล้ว ความหมายของ Express Middleware นั้นถูกกำหนดได้ดีที่สุดใน Express Docs ที่นี่)

ฟังก์ชัน มิดเดิลแวร์ คือฟังก์ชันที่สามารถเข้าถึงออบเจ็กต์คำขอ ( req ) ออบเจ็กต์การตอบสนอง ( res ) และฟังก์ชันมิดเดิลแวร์ถัดไปในวงจรการตอบกลับคำขอของแอปพลิเคชัน ฟังก์ชันมิดเดิลแวร์ถัดไปมักแสดงโดยตัวแปรชื่อ next

ฟังก์ชันมิดเดิลแวร์สามารถทำงานต่อไปนี้:

  • ดำเนินการรหัสใด ๆ
  • ทำการเปลี่ยนแปลงคำขอและวัตถุตอบสนอง
  • สิ้นสุดรอบการตอบรับคำขอ
  • เรียกใช้ฟังก์ชันมิดเดิลแวร์ถัดไปในสแต็ก

กล่าวอีกนัยหนึ่ง ฟังก์ชันมิดเดิลแวร์คือฟังก์ชันแบบกำหนดเองที่เรา (นักพัฒนา) สามารถกำหนดได้ และจะทำหน้าที่เป็นตัวกลางระหว่างเวลาที่ Express ได้รับคำขอและเมื่อฟังก์ชันเรียกกลับที่เหมาะสมของเราเริ่มทำงาน เราอาจสร้างฟังก์ชัน log ตัวอย่างเช่น ที่จะบันทึกทุกครั้งที่มีการร้องขอ โปรดทราบว่าเรายังสามารถเลือกที่จะทำให้ฟังก์ชันมิดเดิลแวร์เหล่านี้เริ่มทำงาน หลังจากที่ จุดสิ้นสุดของเราเริ่มทำงานแล้ว ขึ้นอยู่กับว่าคุณวางไว้ที่ใดในสแต็ก ซึ่งเราจะเห็นในภายหลัง

เพื่อระบุมิดเดิลแวร์ที่กำหนดเอง เราต้องกำหนดเป็นฟังก์ชันและส่งผ่านไปยัง app.use(...)

 const myMiddleware = (req, res, next) => { console.log(`Middleware has fired at time ${Date().now}`); next(); } app.use(myMiddleware); // This is the app variable returned from express().

เมื่อรวมกันแล้ว เรามี:

 // Getting the module from node_modules. const express = require('express'); // Creating our Express Application. const app = express(); // Our middleware function. const myMiddleware = (req, res, next) => { console.log(`Middleware has fired at time ${Date().now}`); next(); } // Tell Express to use the middleware. app.use(myMiddleware); // Defining the port we'll bind to. const PORT = 3000; // Defining a new endpoint behind the "/my-test-route" route. app.get('/my-test-route', () => { console.log('A GET Request was made to /my-test-route.'); }); // Binding the server to port 3000. app.listen(PORT, () => { console.log(`Server is up on port ${PORT}.`) });

หากคุณส่งคำขอผ่านเบราว์เซอร์อีกครั้ง ตอนนี้คุณควรเห็นว่าฟังก์ชันมิดเดิลแวร์ของคุณเริ่มทำงานและบันทึกการประทับเวลา เพื่อส่งเสริมการทดลอง ให้ลองยกเลิกการเรียกใช้ฟังก์ชัน next และดูว่าเกิดอะไรขึ้น

ฟังก์ชันเรียกกลับของมิดเดิลแวร์ถูกเรียกด้วยอาร์กิวเมนต์สามตัว req , res และ next req คือพารามิเตอร์ที่เราข้ามไปเมื่อสร้าง GET Handler ก่อนหน้านี้ และเป็นอ็อบเจ็กต์ที่มีข้อมูลเกี่ยวกับคำขอ เช่น ส่วนหัว ส่วนหัวที่กำหนดเอง พารามิเตอร์ และเนื้อหาใดๆ ที่อาจส่งมาจากไคลเอ็นต์ (เช่น คุณทำกับคำขอ POST) ฉันรู้ว่าเรากำลังพูดถึงมิดเดิลแวร์ที่นี่ แต่ทั้งจุดปลายและฟังก์ชันมิดเดิลแวร์ถูกเรียกด้วย req และ res req และ res จะเหมือนกัน (เว้นแต่จะมีการเปลี่ยนแปลงอย่างใดอย่างหนึ่ง) ทั้งในมิดเดิลแวร์และปลายทางภายในขอบเขตของคำขอเดียวจากไคลเอ็นต์ ซึ่งหมายความว่า ตัวอย่างเช่น คุณสามารถใช้ฟังก์ชันมิดเดิลแวร์เพื่อล้างข้อมูลโดยแยกอักขระที่อาจมุ่งเป้าไปที่การดำเนินการ SQL หรือ NoSQL แล้วส่ง req ที่ปลอดภัยไปยังปลายทาง

res ดังที่เห็นก่อนหน้านี้ อนุญาตให้คุณส่งข้อมูลกลับไปยังไคลเอนต์ได้หลายวิธี

next คือฟังก์ชันเรียกกลับที่คุณต้องดำเนินการเมื่อมิดเดิลแวร์ทำงานเสร็จเพื่อเรียกใช้ฟังก์ชันมิดเดิลแวร์ถัดไปในสแต็กหรือปลายทาง โปรดทราบว่าคุณจะต้อง then สิ่งนี้ในบล็อกของฟังก์ชัน async ที่คุณเรียกใช้ในมิดเดิลแวร์ ขึ้นอยู่กับการดำเนินการ async ของคุณ คุณอาจต้องการหรือไม่ต้องการเรียกมันในบล็อก catch นั่นคือ ฟังก์ชัน myMiddleware ทำงาน หลังจาก ส่งคำขอจากไคลเอ็นต์ แต่ ก่อน ที่ฟังก์ชันจุดสิ้นสุดของคำขอจะเริ่มทำงาน เมื่อเรารันโค้ดนี้และทำการร้องขอ คุณควรเห็นข้อความ Middleware has fired... ก่อนที่ ข้อความ A GET Request was made to... ในคอนโซล ถ้าคุณไม่เรียก next() ส่วนหลังจะไม่ทำงาน — ฟังก์ชันปลายทางของคุณสำหรับคำขอจะไม่เริ่มทำงาน

โปรดทราบด้วยว่าฉันสามารถกำหนดฟังก์ชันนี้โดยไม่ระบุชื่อได้ เช่นนี้ (แบบแผนซึ่งฉันจะยึดติด):

 app.use((req, res, next) => { console.log(`Middleware has fired at time ${Date().now}`); next(); });

สำหรับผู้ที่เพิ่งเริ่มใช้ JavaScript และ ES6 หากวิธีการข้างต้นไม่สมเหตุสมผล ตัวอย่างด้านล่างน่าจะช่วยได้ เราเพียงแค่กำหนดฟังก์ชันเรียกกลับ (ฟังก์ชันที่ไม่ระบุชื่อ) ซึ่งรับฟังก์ชันเรียกกลับอื่น ( next ) เป็นอาร์กิวเมนต์ เราเรียกฟังก์ชันที่รับอาร์กิวเมนต์ของฟังก์ชันเป็นฟังก์ชันลำดับที่สูงกว่า ดูที่ด้านล่าง — มันแสดงให้เห็นตัวอย่างพื้นฐานว่า Express Source Code อาจทำงานอย่างไรเบื้องหลัง:

 console.log('Suppose a request has just been made from the client.\n'); // This is what (it's not exactly) the code behind app.use() might look like. const use = callback => { // Simple log statement to see where we are. console.log('Inside use() - the "use" function has been called.'); // This depicts the termination of the middleware. const next = () => console.log('Terminating Middleware!\n'); // Suppose req and res are defined above (Express provides them). const req = res = null; // "callback" is the "middleware" function that is passed into "use". // "next" is the above function that pretends to stop the middleware. callback(req, res, next); }; // This is analogous to the middleware function we defined earlier. // It gets passed in as "callback" in the "use" function above. const myMiddleware = (req, res, next) => { console.log('Inside the myMiddleware function!'); next(); } // Here, we are actually calling "use()" to see everything work. use(myMiddleware); console.log('Moving on to actually handle the HTTP Request or the next middleware function.');

ก่อนอื่นเราเรียก use ซึ่งรับ myMiddleware เป็นอาร์กิวเมนต์ myMiddleware เป็นฟังก์ชันที่รับอาร์กิวเมนต์สามตัว - req , res และ next Inside use มีการเรียกใช้ myMiddlware และส่งผ่านอาร์กิวเมนต์ทั้งสามนั้น next คือฟังก์ชันที่กำหนดไว้ใน use myMiddleware ถูกกำหนดให้เป็นการ callback ในวิธีการ use ถ้าฉันวาง use ในตัวอย่างนี้ บนวัตถุที่เรียกว่า app เราสามารถเลียนแบบการตั้งค่าของ Express ได้ทั้งหมด แม้ว่าจะไม่มีซ็อกเก็ตหรือการเชื่อมต่อเครือข่ายก็ตาม

ในกรณีนี้ ทั้ง myMiddleware และ callback ต่างก็เป็น Higher Order Functions เนื่องจากทั้งสองใช้ฟังก์ชันเป็นอาร์กิวเมนต์

หากคุณรันโค้ดนี้ คุณจะเห็นการตอบสนองต่อไปนี้:

 Suppose a request has just been made from the client. Inside use() - the "use" function has been called. Inside the middleware function! Terminating Middleware! Moving on to actually handle the HTTP Request or the next middleware function.

โปรดทราบว่าฉันสามารถใช้ฟังก์ชันที่ไม่ระบุตัวตนเพื่อให้ได้ผลลัพธ์เช่นเดียวกัน:

 console.log('Suppose a request has just been made from the client.'); // This is what (it's not exactly) the code behind app.use() might look like. const use = callback => { // Simple log statement to see where we are. console.log('Inside use() - the "use" function has been called.'); // This depicts the termination of the middlewear. const next = () => console.log('Terminating Middlewear!'); // Suppose req and res are defined above (Express provides them). const req = res = null; // "callback" is the function which is passed into "use". // "next" is the above function that pretends to stop the middlewear. callback(req, res, () => { console.log('Terminating Middlewear!'); }); }; // Here, we are actually calling "use()" to see everything work. use((req, res, next) => { console.log('Inside the middlewear function!'); next(); }); console.log('Moving on to actually handle the HTTP Request.');

ด้วยความหวังที่ตกลงกันไว้ ตอนนี้เราสามารถกลับไปที่งานจริงที่มีอยู่แล้ว — ตั้งค่ามิดเดิลแวร์ของเรา

ความจริงของเรื่องนี้คือ โดยทั่วไปคุณจะต้องส่งข้อมูลผ่านคำขอ HTTP คุณมีตัวเลือกที่แตกต่างกันสองสามอย่างในการทำเช่นนั้น — ส่งพารามิเตอร์การค้นหา URL ส่งข้อมูลที่จะสามารถเข้าถึงได้บนวัตถุ req ที่เราได้เรียนรู้เกี่ยวกับก่อนหน้านี้ ฯลฯ วัตถุนั้นไม่เพียงมีให้ในการโทรกลับเพื่อเรียก app.use() แต่ยังรวมถึงจุดปลายใดๆ เราใช้ undefined เป็นตัวเติมก่อนหน้านี้ เพื่อให้เราสามารถมุ่งเน้นไปที่ res ในการส่ง HTML กลับไปยังไคลเอนต์ แต่ตอนนี้ เราต้องการเข้าถึงมัน

 app.use('/my-test-route', (req, res) => { // The req object contains client-defined data that is sent up. // The res object allows the server to send data back down. });

คำขอ HTTP POST อาจ ต้องการให้เราส่งวัตถุเนื้อหาไปยังเซิร์ฟเวอร์ หากคุณมีแบบฟอร์มในไคลเอนต์ และใช้ชื่อผู้ใช้และอีเมลของคุณ มีแนวโน้มว่าคุณจะส่งข้อมูลนั้นไปยังเซิร์ฟเวอร์ในเนื้อหาของคำขอ

มาดูกันว่าฝั่งไคลเอ็นต์จะเป็นอย่างไร:

 <!DOCTYPE html> <html> <body> <form action="https://localhost:3000/email-list" method="POST" > <input type="text" name="nameInput"> <input type="email" name="emailInput"> <input type="submit"> </form> </body> </html>

ทางฝั่งเซิร์ฟเวอร์:

 app.post('/email-list', (req, res) => { // What do we now? // How do we access the values for the user's name and email? });

ในการเข้าถึงชื่อผู้ใช้และอีเมล เราจะต้องใช้มิดเดิลแวร์บางประเภท สิ่งนี้จะใส่ข้อมูลบนวัตถุที่เรียกว่า body available on req Body Parser เป็นวิธีที่ได้รับความนิยมในการทำเช่นนี้ ซึ่งมีให้โดยนักพัฒนา Express เป็นโมดูล NPM แบบสแตนด์อโลน ตอนนี้ Express มาพร้อมกับมิดเดิลแวร์ของตัวเองในการทำเช่นนี้ และเราจะเรียกมันว่าดังนี้:

 app.use(express.urlencoded({ extended: true }));

ตอนนี้เราสามารถทำได้:

 app.post('/email-list', (req, res) => { console.log('User Name: ', req.body.nameInput); console.log('User Email: ', req.body.emailInput); });

ทั้งหมดนี้เป็นการรับอินพุตที่ผู้ใช้กำหนดซึ่งส่งมาจากไคลเอ็นต์ และทำให้พร้อมใช้งานบนอ็อบเจ็กต์ body ของ req โปรดทราบว่าใน req.body ตอนนี้เรามี nameInput และ emailInput ซึ่งเป็นชื่อของแท็ก input ใน HTML ตอนนี้ ข้อมูลที่กำหนดโดยไคลเอ็นต์นี้ควรได้รับการพิจารณาว่าเป็นอันตราย (อย่าเชื่อในไคลเอ็นต์เด็ดขาด) และจำเป็นต้องได้รับการฆ่าเชื้อ แต่เราจะกล่าวถึงในภายหลัง

มิดเดิลแวร์อีกประเภทหนึ่งที่ให้บริการโดย express คือ express.json() express.json ใช้เพื่อจัดแพ็คเกจ JSON Payloads ที่ส่งในคำขอจากลูกค้าไปยัง req.body ในขณะที่ express.urlencoded จะจัดแพ็คเกจคำขอที่เข้ามาด้วยสตริง อาร์เรย์ หรือข้อมูลที่เข้ารหัส URL อื่น ๆ บน req.body กล่าวโดยย่อ ทั้งจัดการ req.body แต่ . .json() ใช้สำหรับ JSON Payloads และ .urlencoded() ใช้สำหรับพารามิเตอร์ POST Query

อีกวิธีหนึ่งในการพูดนี้คือคำขอที่เข้ามาด้วย Content-Type: application/json header (เช่นการระบุ POST Body ด้วย fetch API) จะถูกจัดการโดย express.json() ในขณะที่คำขอที่มีส่วนหัว Content-Type: application/x-www-form-urlencoded (เช่น แบบฟอร์ม HTML) จะได้รับการจัดการด้วย express.urlencoded() หวังว่าตอนนี้จะสมเหตุสมผล

การเริ่มต้นเส้นทาง CRUD ของเราสำหรับ MongoDB

หมายเหตุ : เมื่อดำเนินการคำขอ PATCH ในบทความนี้ เราจะไม่ปฏิบัติตามข้อกำหนด JSONPatch RFC — ปัญหาที่เราจะแก้ไขในบทความถัดไปของชุดนี้

เมื่อพิจารณาว่าเราเข้าใจว่าเราระบุปลายทางแต่ละจุดโดยเรียกใช้ฟังก์ชันที่เกี่ยวข้องใน app ส่งเส้นทางและฟังก์ชันเรียกกลับที่มีออบเจ็กต์คำขอและการตอบสนอง เราสามารถเริ่มกำหนดเส้นทาง CRUD ของเราสำหรับ API ชั้นวางหนังสือได้ อันที่จริง และเมื่อพิจารณาว่านี่เป็นบทความเบื้องต้น ฉันจะไม่สนใจที่จะปฏิบัติตามข้อกำหนด HTTP และ REST อย่างสมบูรณ์ และฉันจะไม่พยายามใช้สถาปัตยกรรมที่สะอาดที่สุดเท่าที่จะเป็นไปได้ ที่จะมาในบทความต่อๆ ไป

ฉันจะเปิดไฟล์ server.js ที่เราใช้มาจนถึงตอนนี้และล้างข้อมูลทุกอย่างเพื่อเริ่มต้นจากกระดานชนวนที่สะอาดด้านล่าง:

 // Getting the module from node_modules. const express = require('express'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true )); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // ... // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

พิจารณาโค้ดต่อไปนี้ทั้งหมดเพื่อใช้ส่วน // ... ของไฟล์ด้านบน

เพื่อกำหนดปลายทางของเรา และเนื่องจากเรากำลังสร้าง REST API เราควรพูดถึงวิธีที่เหมาะสมในการตั้งชื่อเส้นทาง อีกครั้ง คุณควรดูที่ส่วน HTTP ของบทความเก่าของฉันสำหรับข้อมูลเพิ่มเติม เรากำลังจัดการกับหนังสือ ดังนั้นเส้นทางทั้งหมดจะอยู่ด้านหลัง /books (หลักการตั้งชื่อพหูพจน์เป็นมาตรฐาน)

ขอ เส้นทาง
โพสต์ /books
รับ /books/id
ปะ /books/id
ลบ /books/id

อย่างที่คุณเห็น ไม่จำเป็นต้องระบุ ID เมื่อโพสต์หนังสือเพราะเราจะ (หรือมากกว่า MongoDB) จะสร้างให้เราโดยอัตโนมัติทางฝั่งเซิร์ฟเวอร์ การรับ แพตช์ และการลบหนังสือทั้งหมดต้องการให้เราส่ง ID นั้นไปยังปลายทางของเรา ซึ่งเราจะหารือในภายหลัง ในตอนนี้ เรามาสร้างจุดปลายกัน:

 // HTTP POST /books app.post('/books', (req, res) => { // ... console.log('A POST Request was made!'); }); // HTTP GET /books/:id app.get('/books/:id', (req, res) => { // ... console.log(`A GET Request was made! Getting book ${req.params.id}`); }); // HTTP PATCH /books/:id app.patch('/books/:id', (req, res) => { // ... console.log(`A PATCH Request was made! Updating book ${req.params.id}`); }); // HTTP DELETE /books/:id app.delete('/books/:id', (req, res) => { // ... console.log(`A DELETE Request was made! Deleting book ${req.params.id}`); });

ไวยากรณ์ :id บอก Express ว่า id เป็นพารามิเตอร์ไดนามิกที่จะถูกส่งต่อใน URL เราสามารถเข้าถึงได้บนวัตถุ params ซึ่งมีอยู่ใน req ฉันรู้ว่า "เราสามารถเข้าถึงได้ตาม req " ดูเหมือนว่าเวทมนตร์และเวทมนตร์ (ซึ่งไม่มีอยู่จริง) เป็นสิ่งที่อันตรายในการเขียนโปรแกรม แต่คุณต้องจำไว้ว่า Express ไม่ใช่กล่องดำ เป็นโครงการโอเพ่นซอร์สที่มีอยู่ใน GitHub ภายใต้ MIT LIcense คุณสามารถดูซอร์สโค้ดได้อย่างง่ายดาย หากคุณต้องการดูว่าพารามิเตอร์เคียวรีไดนามิกถูกใส่ลงในออบเจกต์ req อย่างไร

เมื่อรวมกันแล้ว เรามีสิ่งต่อไปนี้ในไฟล์ server.js ของเรา:

 // Getting the module from node_modules. const express = require('express'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // HTTP POST /books app.post('/books', (req, res) => { // ... console.log('A POST Request was made!'); }); // HTTP GET /books/:id app.get('/books/:id', (req, res) => { // ... console.log(`A GET Request was made! Getting book ${req.params.id}`); }); // HTTP PATCH /books/:id app.patch('/books/:id', (req, res) => { // ... console.log(`A PATCH Request was made! Updating book ${req.params.id}`); }); // HTTP DELETE /books/:id app.delete('/books/:id', (req, res) => { // ... console.log(`A DELETE Request was made! Deleting book ${req.params.id}`); }); // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

ไปข้างหน้าและเริ่มต้นเซิร์ฟเวอร์ เรียกใช้ node server.js จากเทอร์มินัลหรือบรรทัดคำสั่ง และไปที่เบราว์เซอร์ของคุณ เปิด Chrome Development Console และในแถบ URL (Uniform Resource Locator) ให้ไปที่ localhost:3000/books คุณควรเห็นตัวบ่งชี้ในเทอร์มินัลของระบบปฏิบัติการของคุณแล้วว่าเซิร์ฟเวอร์ทำงานอยู่ เช่นเดียวกับคำสั่งบันทึกสำหรับ GET

จนถึงตอนนี้ เราใช้เว็บเบราว์เซอร์เพื่อดำเนินการคำขอ GET นั่นเป็นสิ่งที่ดีสำหรับเพิ่งเริ่มต้น แต่เราจะพบเครื่องมือที่ดีกว่าในการทดสอบเส้นทาง API ได้อย่างรวดเร็ว อันที่จริง เราสามารถวางสาย fetch เข้าโดยตรงในคอนโซลหรือใช้บริการออนไลน์บางอย่าง ในกรณีของเรา และเพื่อเป็นการประหยัดเวลา เราจะใช้ cURL และบุรุษไปรษณีย์ ฉันใช้ทั้งสองอย่างในบทความนี้ (แม้ว่าคุณสามารถใช้อย่างใดอย่างหนึ่งหรือ) เพื่อที่ฉันจะได้แนะนำพวกเขาหากคุณยังไม่ได้ใช้ cURL เป็นไลบรารี่ (ไลบรารีที่สำคัญมาก) และเครื่องมือบรรทัดคำสั่งที่ออกแบบมาเพื่อถ่ายโอนข้อมูลโดยใช้โปรโตคอลต่างๆ บุรุษไปรษณีย์เป็นเครื่องมือที่ใช้ GUI สำหรับทดสอบ API หลังจากทำตามคำแนะนำในการติดตั้งที่เกี่ยวข้องสำหรับเครื่องมือทั้งสองบนระบบปฏิบัติการของคุณแล้ว ตรวจสอบให้แน่ใจว่าเซิร์ฟเวอร์ของคุณยังคงทำงานอยู่ จากนั้นรันคำสั่งต่อไปนี้ (ทีละรายการ) ในเทอร์มินัลใหม่ สิ่งสำคัญคือคุณต้องพิมพ์และดำเนินการทีละรายการ จากนั้นดูข้อความบันทึกในเทอร์มินัลที่แยกจากเซิร์ฟเวอร์ของคุณ นอกจากนี้ โปรดทราบว่าสัญลักษณ์ความคิดเห็นของภาษาโปรแกรมมาตรฐาน // ไม่ใช่สัญลักษณ์ที่ถูกต้องใน Bash หรือ MS-DOS คุณจะต้องละบรรทัดเหล่านั้น และฉันใช้เฉพาะที่นี่เพื่ออธิบายแต่ละบล็อกของคำสั่ง cURL

 // HTTP POST Request (Localhost, IPv4, IPv6) curl -X POST https://localhost:3000/books curl -X POST https://127.0.0.1:3000/books curl -X POST https://[::1]:3000/books // HTTP GET Request (Localhost, IPv4, IPv6) curl -X GET https://localhost:3000/books/123abc curl -X GET https://127.0.0.1:3000/books/book-id-123 curl -X GET https://[::1]:3000/books/book-abc123 // HTTP PATCH Request (Localhost, IPv4, IPv6) curl -X PATCH https://localhost:3000/books/456 curl -X PATCH https://127.0.0.1:3000/books/218 curl -X PATCH https://[::1]:3000/books/some-id // HTTP DELETE Request (Localhost, IPv4, IPv6) curl -X DELETE https://localhost:3000/books/abc curl -X DELETE https://127.0.0.1:3000/books/314 curl -X DELETE https://[::1]:3000/books/217

อย่างที่คุณเห็น ID ที่ส่งผ่านเป็นพารามิเตอร์ URL สามารถเป็นค่าใดก็ได้ แฟล็ก -X ระบุประเภทของคำขอ HTTP (สามารถละเว้นได้สำหรับ GET) และเราจัดเตรียม URL ที่จะทำการร้องขอหลังจากนั้น ฉันทำซ้ำแต่ละคำขอสามครั้ง เพื่อให้คุณเห็นว่าทุกอย่างยังคงใช้งานได้ไม่ว่าคุณจะใช้ชื่อโฮสต์ localhost ที่อยู่ IPv4 ( 127.0.0.1 ) ที่ localhost แก้ไข หรือที่อยู่ IPv6 ( ::1 ) ที่ localhost แก้ไข . โปรดทราบว่า cURL ต้องการการตัดที่อยู่ IPv6 ในวงเล็บเหลี่ยม

ตอนนี้เราอยู่ในที่ที่เหมาะสม — เรามีโครงสร้างที่เรียบง่ายของเส้นทางและปลายทางของเราที่ตั้งค่าไว้ เซิร์ฟเวอร์ทำงานอย่างถูกต้องและยอมรับคำขอ HTTP ตามที่เราคาดหวัง ตรงกันข้ามกับสิ่งที่คุณคาดหวัง มีเวลาอีกไม่นานที่จะถึงจุดนี้ — เราเพียงแค่ต้องตั้งค่าฐานข้อมูลของเรา โฮสต์มัน (โดยใช้ฐานข้อมูลในฐานะบริการ — MongoDB Atlas) และรักษาข้อมูลไว้ (และ ดำเนินการตรวจสอบและสร้างการตอบกลับข้อผิดพลาด)

การตั้งค่าฐานข้อมูล MongoDB การผลิต

ในการตั้งค่าฐานข้อมูลที่ใช้งานจริง เราจะไปที่หน้าแรกของ MongoDB Atlas และลงชื่อสมัครใช้บัญชีฟรี หลังจากนั้น ให้สร้างคลัสเตอร์ใหม่ คุณสามารถคงการตั้งค่าเริ่มต้นไว้ได้ โดยเลือกระดับค่าธรรมเนียมตามภูมิภาคที่เกี่ยวข้อง จากนั้นกดปุ่ม "สร้างคลัสเตอร์" คลัสเตอร์จะใช้เวลาสักครู่ในการสร้าง จากนั้นคุณจะสามารถได้รับ URL ฐานข้อมูลและรหัสผ่านของคุณ จดสิ่งเหล่านี้เมื่อคุณเห็นพวกเขา เราจะฮาร์ดโค้ดให้พวกเขาก่อน แล้วจึงจัดเก็บไว้ในตัวแปรสภาพแวดล้อมในภายหลังเพื่อความปลอดภัย สำหรับความช่วยเหลือในการสร้างและเชื่อมต่อกับคลัสเตอร์ ฉันจะแนะนำเอกสาร MongoDB ให้คุณโดยเฉพาะหน้านี้และหน้านี้ หรือคุณสามารถแสดงความคิดเห็นด้านล่างและฉันจะพยายามช่วย

การสร้างแบบจำลองพังพอน

ขอแนะนำให้คุณมีความเข้าใจในความหมายของเอกสารและคอลเลกชั่นในบริบทของ NoSQL (ไม่ใช่แค่ SQL — Structured Query Language) สำหรับการอ้างอิง คุณอาจต้องการอ่านทั้ง Mongoose Quick Start Guide และส่วน MongoDB ของบทความเก่าของฉัน

ขณะนี้เรามีฐานข้อมูลที่พร้อมรับ CRUD Operations Mongoose เป็นโมดูลโหนด (หรือ ODM — Object Document Mapper) ที่จะช่วยให้เราดำเนินการดังกล่าวได้ (ขจัดความซับซ้อนบางอย่างออกไป) รวมทั้งตั้งค่าสคีมาหรือโครงสร้างของการรวบรวมฐานข้อมูล

ในฐานะข้อจำกัดความรับผิดชอบที่สำคัญ มีการโต้เถียงกันมากมายเกี่ยวกับ ORM และรูปแบบต่างๆ เช่น Active Record หรือ Data Mapper นักพัฒนาบางคนสาบานด้วย ORM และคนอื่น ๆ สาบานกับพวกเขา (เชื่อว่าพวกเขาเข้ามาขวางทาง) สิ่งสำคัญที่ควรทราบก็คือ ORM นั้นแยกส่วนออกไปมาก เช่น การรวมการเชื่อมต่อ การเชื่อมต่อซ็อกเก็ต และการจัดการ ฯลฯ คุณสามารถใช้ MongoDB Native Driver (โมดูล NPM อื่น) ได้อย่างง่ายดาย แต่มันจะใช้งานได้ดีกว่ามาก แม้ว่าจะแนะนำให้คุณเล่นกับ Native Driver ก่อนใช้ ORM แต่ฉันละเว้น Native Driver ที่นี่เพื่อความกระชับ สำหรับการดำเนินการ SQL ที่ซับซ้อนบนฐานข้อมูลเชิงสัมพันธ์นั้น ORM บางตัวจะไม่ได้รับการปรับให้เหมาะสมกับความเร็วของคิวรี และคุณอาจจบลงด้วยการเขียน SQL ดิบของคุณเอง ORMs can come into play a lot with Domain-Driven Design and CQRS, among others. They are an established concept in the .NET world, and the Node.js community has not completely caught up yet — TypeORM is better, but it's not NHibernate or Entity Framework.

To create our Model, I'll create a new folder in the server directory entitled models , within which I'll create a single file with the name book.js . Thus far, our project's directory structure is as follows:

 - server - node_modules - models - book.js - package.json - server.js

Indeed, this directory structure is not required, but I use it here because it's simple. Allow me to note that this is not at all the kind of architecture you want to use for larger applications (and you might not even want to use JavaScript — TypeScript could be a better option), which I discuss in this article's closing. The next step will be to install mongoose , which is performed via, as you might expect, npm i mongoose .

The meaning of a Model is best ascertained from the Mongoose documentation:

Models are fancy constructors compiled from Schema definitions. An instance of a model is called a document. Models are responsible for creating and reading documents from the underlying MongoDB database.

Before creating the Model, we'll define its Schema. A Schema will, among others, make certain expectations about the value of the properties provided. MongoDB is schemaless, and thus this functionality is provided by the Mongoose ODM. Let's start with a simple example. Suppose I want my database to store a user's name, email address, and password. Traditionally, as a plain old JavaScript Object (POJO), such a structure might look like this:

 const userDocument = { name: 'Jamie Corkhill', email: '[email protected]', password: 'Bcrypt Hash' };

If that above object was how we expected our user's object to look, then we would need to define a schema for it, like this:

 const schema = { name: { type: String, trim: true, required: true }, email: { type: String, trim: true, required: true }, password: { type: String, required: true } };

Notice that when creating our schema, we define what properties will be available on each document in the collection as an object in the schema. In our case, that's name , email , and password . The fields type , trim , required tell Mongoose what data to expect. If we try to set the name field to a number, for example, or if we don't provide a field, Mongoose will throw an error (because we are expecting a type of String ), and we can send back a 400 Bad Request to the client. This might not make sense right now because we have defined an arbitrary schema object. However, the fields of type , trim , and required (among others) are special validators that Mongoose understands. trim , for example, will remove any whitespace from the beginning and end of the string. We'll pass the above schema to mongoose.Schema() in the future and that function will know what to do with the validators.

Understanding how Schemas work, we'll create the model for our Books Collection of the Bookshelf API. Let's define what data we require:

  1. ชื่อ

  2. ISBN Number

  3. ผู้เขียน

    1. ชื่อจริง

    2. นามสกุล

  4. Publishing Date

  5. Finished Reading (Boolean)

I'm going to create this in the book.js file we created earlier in /models . Like the example above, we'll be performing validation:

 const mongoose = require('mongoose'); // Define the schema: const mySchema = { title: { type: String, required: true, trim: true, }, isbn: { type: String, required: true, trim: true, }, author: { firstName:{ type: String, required: true, trim: true }, lastName: { type: String, required: true, trim: true } }, publishingDate: { type: String }, finishedReading: { type: Boolean, required: true, default: false } }

default will set a default value for the property if none is provided — finishedReading for example, although a required field, will be set automatically to false if the client does not send one up.

Mongoose also provides the ability to perform custom validation on our fields, which is done by supplying the validate() method, which attains the value that was attempted to be set as its one and only parameter. In this function, we can throw an error if the validation fails. นี่คือตัวอย่าง:

 // ... isbn: { type: String, required: true, trim: true, validate(value) { if (!validator.isISBN(value)) { throw new Error('ISBN is invalid.'); } } } // ...

Now, if anyone supplies an invalid ISBN to our model, Mongoose will throw an error when trying to save that document to the collection. I've already installed the NPM module validator via npm i validator and required it. validator contains a bunch of helper functions for common validation requirements, and I use it here instead of RegEx because ISBNs can't be validated with RegEx alone due to a tailing checksum. Remember, users will be sending a JSON body to one of our POST routes. That endpoint will catch any errors (such as an invalid ISBN) when attempting to save, and if one is thrown, it'll return a blank response with an HTTP 400 Bad Request status — we haven't yet added that functionality.

Finally, we have to define our schema of earlier as the schema for our model, so I'll make a call to mongoose.Schema() passing in that schema:

 const bookSchema = mongoose.Schema(mySchema);

To make things more precise and clean, I'll replace the mySchema variable with the actual object all on one line:

 const bookSchema = mongoose.Schema({ title:{ type: String, required: true, trim: true, }, isbn:{ type: String, required: true, trim: true, validate(value) { if (!validator.isISBN(value)) { throw new Error('ISBN is invalid.'); } } }, author:{ firstName: { type: String required: true, trim: true }, lastName:{ type: String, required: true, trim: true } }, publishingDate:{ type: String }, finishedReading:{ type: Boolean, required: true, default: false } });

Let's take a final moment to discuss this schema. We are saying that each of our documents will consist of a title, an ISBN, an author with a first and last name, a publishing date, and a finishedReading boolean.

  1. title will be of type String , it's a required field, and we'll trim any whitespace.
  2. isbn will be of type String , it's a required field, it must match the validator, and we'll trim any whitespace.
  3. author is of type object containing a required, trimmed, string firstName and a required, trimmed, string lastName.
  4. publishingDate is of type String (although we could make it of type Date or Number for a Unix timestamp.
  5. finishedReading is a required boolean that will default to false if not provided.

With our bookSchema defined, Mongoose knows what data and what fields to expect within each document to the collection that stores books. However, how do we tell it what collection that specific schema defines? We could have hundreds of collections, so how do we correlate, or tie, bookSchema to the Book collection?

The answer, as seen earlier, is with the use of models. We'll use bookSchema to create a model, and that model will model the data to be stored in the Book collection, which will be created by Mongoose automatically.

Append the following lines to the end of the file:

 const Book = mongoose.model('Book', bookSchema); module.exports = Book;

As you can see, we have created a model, the name of which is Book (— the first parameter to mongoose.model() ), and also provided the ruleset, or schema, to which all data is saved in the Book collection will have to abide. We export this model as a default export, allowing us to require the file for our endpoints to access. Book is the object upon which we'll call all of the required functions to Create, Read, Update, and Delete data which are provided by Mongoose.

Altogether, our book.js file should look as follows:

 const mongoose = require('mongoose'); const validator = require('validator'); // Define the schema. const bookSchema = mongoose.Schema({ title:{ type: String, required: true, trim: true, }, isbn:{ type: String, required: true, trim: true, validate(value) { if (!validator.isISBN(value)) { throw new Error('ISBN is invalid.'); } } }, author:{ firstName: { type: String, required: true, trim: true }, lastName:{ type: String, required: true, trim: true } }, publishingDate:{ type: String }, finishedReading:{ type: Boolean, required: true, default: false } }); // Create the "Book" model of name Book with schema bookSchema. const Book = mongoose.model('Book', bookSchema); // Provide the model as a default export. module.exports = Book;

Connecting To MongoDB (Basics)

Don't worry about copying down this code. I'll provide a better version in the next section. To connect to our database, we'll have to provide the database URL and password. We'll call the connect method available on mongoose to do so, passing to it the required data. For now, we are going hardcode the URL and password — an extremely frowned upon technique for many reasons: namely the accidental committing of sensitive data to a public (or private made public) GitHub Repository. Realize also that commit history is saved, and that if you accidentally commit a piece of sensitive data, removing it in a future commit will not prevent people from seeing it (or bots from harvesting it), because it's still available in the commit history. CLI tools exist to mitigate this issue and remove history.

As stated, for now, we'll hard code the URL and password, and then save them to environment variables later. At this point, let's look at simply how to do this, and then I'll mention a way to optimize it.

const mongoose = require('mongoose'); const MONGODB_URL = 'Your MongoDB URL'; mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false, useUnifiedTopology: true });

นี้จะเชื่อมต่อกับฐานข้อมูล เราจัดเตรียม URL ที่เราได้รับจากแดชบอร์ด MongoDB Atlas และอ็อบเจ็กต์ที่ส่งผ่านเข้ามาเมื่อพารามิเตอร์ตัวที่สองระบุคุณสมบัติที่จะใช้เพื่อป้องกันคำเตือนการเลิกใช้งาน

Mongoose ซึ่งใช้ MongoDB Native Driver หลักอยู่เบื้องหลัง จะต้องพยายามติดตามการเปลี่ยนแปลงที่เกิดขึ้นกับไดรเวอร์ ในเวอร์ชันใหม่ของไดรเวอร์ กลไกที่ใช้ในการแยกวิเคราะห์ URL การเชื่อมต่อมีการเปลี่ยนแปลง ดังนั้นเราจึงส่ง useNewUrlParser: true เพื่อระบุว่าเราต้องการใช้เวอร์ชันล่าสุดจากไดรเวอร์อย่างเป็นทางการ

ตามค่าเริ่มต้น หากคุณตั้งค่าดัชนี (และเรียกว่า “ดัชนี” ไม่ใช่ “ดัชนี”) (ซึ่งเราจะไม่กล่าวถึงในบทความนี้) กับข้อมูลในฐานข้อมูลของคุณ Mongoose จะใช้ ensureIndex() ที่มีให้จาก Native Driver MongoDB เลิกใช้ฟังก์ชันนั้นเพื่อสนับสนุน createIndex() ดังนั้นการตั้งค่าสถานะ useCreateIndex เป็น true จะบอก Mongoose ให้ใช้ createIndex() จากไดรเวอร์ ซึ่งเป็นฟังก์ชันที่ไม่เลิกใช้

findOneAndUpdate เวอร์ชันดั้งเดิมของ Mongoose (ซึ่งเป็นวิธีการค้นหาเอกสารในฐานข้อมูลและอัปเดต) เป็นเวอร์ชันก่อนหน้าของเวอร์ชัน Native Driver นั่นคือ findOneAndUpdate() ไม่ใช่ฟังก์ชัน Native Driver แต่เดิมเป็นฟังก์ชันที่ Mongoose จัดหาให้ ดังนั้น Mongoose จึงต้องใช้ findAndModify ที่ไดรเวอร์จัดเตรียมให้เบื้องหลังเพื่อสร้างฟังก์ชัน findOneAndUpdate เมื่ออัปเดตไดรเวอร์แล้ว มันมีฟังก์ชั่นดังกล่าว ดังนั้นเราจึงไม่จำเป็นต้องใช้ findAndModify สิ่งนี้อาจไม่สมเหตุสมผล และไม่เป็นไร ไม่ใช่ข้อมูลสำคัญเกี่ยวกับขนาดของสิ่งต่างๆ

สุดท้าย MongoDB เลิกใช้เซิร์ฟเวอร์เก่าและระบบตรวจสอบเครื่องยนต์ เราใช้วิธีการใหม่กับ useUnifiedTopology: true

สิ่งที่เรามีคือวิธีการเชื่อมต่อกับฐานข้อมูล แต่นี่คือสิ่งที่ — ไม่สามารถปรับขนาดได้หรือมีประสิทธิภาพ เมื่อเราเขียนการทดสอบหน่วยสำหรับ API นี้ การทดสอบหน่วยจะใช้ข้อมูลการทดสอบของตนเอง (หรือส่วนควบ) ในฐานข้อมูลการทดสอบของตนเอง ดังนั้นเราจึงต้องการวิธีที่จะสร้างการเชื่อมต่อเพื่อวัตถุประสงค์ที่แตกต่างกัน — บางอย่างสำหรับสภาพแวดล้อมการทดสอบ (ซึ่งเราสามารถหมุนและแยกส่วนได้ตามต้องการ) อื่นๆ สำหรับสภาพแวดล้อมการพัฒนา และอื่นๆ สำหรับสภาพแวดล้อมการใช้งานจริง ในการทำเช่นนั้น เราจะสร้างโรงงาน (จำเรื่องเมื่อก่อนได้ไหม)

การเชื่อมต่อกับ Mongo — การสร้างการใช้งานโรงงาน JS

แท้จริงแล้ว Java Objects นั้นไม่เหมือนกับ JavaScript Objects เลย ดังนั้นสิ่งที่เรารู้ข้างต้นจาก Factory Design Pattern จะไม่นำมาใช้ ข้าพเจ้าเพียงแต่ยกตัวอย่างเพื่อแสดงลวดลายดั้งเดิม ในการบรรลุวัตถุใน Java หรือ C# หรือ C++ เป็นต้น เราต้องสร้างอินสแตนซ์ของคลาส เสร็จสิ้นด้วยคีย์เวิร์ด new ซึ่งสั่งให้คอมไพเลอร์จัดสรรหน่วยความจำสำหรับอ็อบเจ็กต์บนฮีป ใน C++ สิ่งนี้ทำให้เรามีตัวชี้ไปยังวัตถุที่เราต้องทำความสะอาดตัวเอง ดังนั้นเราจึงไม่มีพอยน์เตอร์ที่ค้างหรือหน่วยความจำรั่ว (C++ ไม่มีตัวรวบรวมขยะ ต่างจาก Node/V8 ที่สร้างบน C++) ใน JavaScript ข้างต้นไม่จำเป็นต้องทำ — เราไม่จำเป็นต้องยกตัวอย่างคลาสเพื่อบรรลุวัตถุ — วัตถุเป็นเพียง {} บางคนจะบอกว่าทุกอย่างใน JavaScript เป็นวัตถุ แม้ว่าในทางเทคนิคจะไม่เป็นความจริงเพราะประเภทดั้งเดิมไม่ใช่วัตถุ

ด้วยเหตุผลข้างต้น โรงงาน JS ของเราจะง่ายกว่า โดยยึดคำจำกัดความแบบหลวมๆ ของโรงงานว่าเป็นฟังก์ชันที่ส่งคืนวัตถุ (วัตถุ JS) เนื่องจากฟังก์ชันเป็นอ็อบเจ็กต์ (สำหรับ function ที่สืบทอดมาจาก object ผ่านการสืบทอดต้นแบบ) ตัวอย่างด้านล่างของเราจะเป็นไปตามเกณฑ์นี้ ในการปรับใช้โรงงาน ฉันจะสร้างโฟลเดอร์ใหม่ภายใน server ชื่อ db ภายใน db ฉันจะสร้างไฟล์ใหม่ชื่อ mongoose.js ไฟล์นี้จะทำการเชื่อมต่อกับฐานข้อมูล ภายใน mongoose.js ฉันจะสร้างฟังก์ชันชื่อ connectionFactory และส่งออกโดยค่าเริ่มต้น:

 // Directory - server/db/mongoose.js const mongoose = require('mongoose'); const MONGODB_URL = 'Your MongoDB URL'; const connectionFactory = () => { return mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false }); }; module.exports = connectionFactory;

การใช้ชวเลขที่ ES6 จัดเตรียมไว้สำหรับ Arrow Functions ที่ส่งคืนคำสั่งเดียวในบรรทัดเดียวกับลายเซ็นเมธอด ฉันจะทำให้ไฟล์นี้ง่ายขึ้นโดยกำจัดคำนิยามของ connectionFactory และเพียงแค่ส่งออกค่าเริ่มต้นจากโรงงาน:

 // server/db/mongoose.js const mongoose = require('mongoose'); const MONGODB_URL = 'Your MongoDB URL'; module.exports = () => mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: true });

ตอนนี้ สิ่งที่ต้องทำคือต้องการไฟล์และเรียกใช้เมธอดที่เอ็กซ์พอร์ตดังนี้:

 const connectionFactory = require('./db/mongoose'); connectionFactory(); // OR require('./db/mongoose')();

คุณสามารถกลับการควบคุมโดยให้ MongoDB URL ของคุณเป็นพารามิเตอร์สำหรับฟังก์ชันโรงงาน แต่เรากำลังจะเปลี่ยน URL แบบไดนามิกเป็นตัวแปรสภาพแวดล้อมตามสภาพแวดล้อม

ประโยชน์ของการเชื่อมต่อเป็นฟังก์ชันคือ เราสามารถเรียกใช้ฟังก์ชันนั้นในโค้ดในภายหลังเพื่อเชื่อมต่อกับฐานข้อมูลจากไฟล์ที่มุ่งเป้าไปที่การผลิต และที่มุ่งเป้าไปที่การทดสอบการรวมระบบภายในและระยะไกลทั้งบนอุปกรณ์และด้วยไปป์ไลน์ CI/CD ระยะไกล / สร้างเซิร์ฟเวอร์

การสร้างปลายทางของเรา

ตอนนี้เราเริ่มเพิ่มตรรกะที่เกี่ยวข้องกับ CRUD แบบง่ายๆ ให้กับปลายทางของเรา ตามที่ระบุไว้ก่อนหน้านี้ข้อจำกัดความรับผิดชอบสั้น ๆ อยู่ในลำดับ วิธีการที่เราดำเนินการเกี่ยวกับการใช้ตรรกะทางธุรกิจของเราในที่นี้ ไม่ใช่ วิธีที่คุณควรจะสะท้อนให้เห็นอย่างอื่นนอกจากโครงการทั่วไป การเชื่อมต่อกับฐานข้อมูลและการดำเนินการทางตรรกะโดยตรงภายในปลายทางนั้น (และควรเป็น) ขมวดคิ้วเพราะคุณสูญเสียความสามารถในการสลับบริการหรือ DBMS โดยไม่ต้องดำเนินการ refactor ทั่วทั้งแอปพลิเคชัน อย่างไรก็ตาม เมื่อพิจารณาว่าบทความนี้เป็นบทความสำหรับผู้เริ่มต้น ฉันจึงใช้แนวทางปฏิบัติที่ไม่ดีเหล่านี้ที่นี่ บทความในอนาคตในชุดนี้จะกล่าวถึงวิธีที่เราสามารถเพิ่มทั้งความซับซ้อนและคุณภาพของสถาปัตยกรรมของเรา

ในตอนนี้ ให้กลับไปที่ไฟล์ server.js ของเราและตรวจดูให้แน่ใจว่าเราทั้งคู่มีจุดเริ่มต้นเหมือนกัน สังเกตว่า ฉันได้เพิ่มคำสั่ง require สำหรับโรงงานเชื่อมต่อฐานข้อมูลของเรา และฉันนำเข้าโมเดลที่เราส่งออกจาก . ./models/book.js

 const express = require('express'); // Database connection and model. require('./db/mongoose.js'); const Book = require('./models/book.js'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // HTTP POST /books app.post('/books', (req, res) => { // ... console.log('A POST Request was made!'); }); // HTTP GET /books/:id app.get('/books/:id', (req, res) => { // ... console.log(`A GET Request was made! Getting book ${req.params.id}`); }); // HTTP PATCH /books/:id app.patch('/books/:id', (req, res) => { // ... console.log(`A PATCH Request was made! Updating book ${req.params.id}`); }); // HTTP DELETE /books/:id app.delete('/books/:id', (req, res) => { // ... console.log(`A DELETE Request was made! Deleting book ${req.params.id}`); }); // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

ฉันจะเริ่มต้นด้วย app.post() เราสามารถเข้าถึงโมเดล Book ได้เนื่องจากเราส่งออกจากไฟล์ที่เราสร้างขึ้น ตามที่ระบุไว้ในเอกสาร Mongoose Book สามารถสร้างได้ ในการสร้างหนังสือเล่มใหม่ เราเรียก Constructor และส่งข้อมูลหนังสือดังนี้:

 const book = new Book(bookData);

ในกรณีของเรา เราจะมี bookData เป็นวัตถุที่ส่งในคำขอ ซึ่งจะอยู่ใน req.body.book โปรดจำไว้ว่า มิดเดิลแวร์ express.json() จะใส่ข้อมูล JSON ที่เราส่งขึ้นไปบน req.body เราจะส่ง JSON ในรูปแบบต่อไปนี้:

 { "book": { "title": "The Art of Computer Programming", "isbn": "ISBN-13: 978-0-201-89683-1", "author": { "firstName": "Donald", "lastName": "Knuth" }, "publishingDate": "July 17, 1997", "finishedReading": true } }

นั่นหมายความว่า JSON ที่เราส่งต่อจะได้รับการแยกวิเคราะห์ และวัตถุ JSON ทั้งหมด (วงเล็บปีกกาคู่แรก) จะถูกวางบน req.body โดยมิดเดิลแวร์ express.json() คุณสมบัติหนึ่งเดียวบนวัตถุ JSON ของเราคือ book ดังนั้นวัตถุ book จะพร้อมใช้งานบน req.body.book

ณ จุดนี้ เราสามารถเรียกฟังก์ชันตัวสร้างแบบจำลองและส่งข้อมูลของเรา:

 app.post('/books', async (req, res) => { // <- Notice 'async' const book = new Book(req.body.book); await book.save(); // <- Notice 'await' });

สังเกตบางสิ่งที่นี่ การเรียกใช้เมธอด save ในอินสแตนซ์ที่เราได้กลับมาจากการเรียกฟังก์ชัน constructor จะทำให้อ็อบเจ็กต์ req.body.book ยังคงอยู่ในฐานข้อมูล หากเป็นไปตามสคีมาที่เรากำหนดไว้ในโมเดล Mongoose การบันทึกข้อมูลลงในฐานข้อมูลเป็นการดำเนินการแบบอะซิงโครนัส และวิธีการ save() นี้จะส่งคืนสัญญา ซึ่งเป็นการชำระที่เรารอคอยมาก แทนที่จะใช้ลูกโซ่ในการเรียก .then() ฉันใช้ไวยากรณ์ ES6 Async/Await ซึ่งหมายความว่าฉันต้องทำให้ฟังก์ชันการโทรกลับเป็น app.post async

book.save() จะปฏิเสธด้วย ValidationError หากวัตถุที่ไคลเอนต์ส่งไม่สอดคล้องกับสคีมาที่เรากำหนด การตั้งค่าปัจจุบันของเราทำให้เกิดโค้ดที่ไม่สม่ำเสมอและเขียนได้ไม่ดี เนื่องจากเราไม่ต้องการให้แอปพลิเคชันของเราขัดข้องในกรณีที่เกิดความล้มเหลวเกี่ยวกับการตรวจสอบความถูกต้อง ฉันจะแก้ไขการดำเนินการที่เป็นอันตรายในประโยคการ try/catch ในกรณีที่เกิดข้อผิดพลาด ฉันจะส่งคืน HTTP 400 Bad Request หรือ HTTP 422 Unprocessable Entity มีการถกเถียงกันอยู่บ้างว่าควรใช้อะไร ดังนั้นฉันจะใช้ 400 สำหรับบทความนี้เนื่องจากเป็นบทความทั่วไปมากกว่า

 app.post('/books', async (req, res) => { try { const book = new Book(req.body.book); await book.save(); return res.status(201).send({ book }); } catch (e) { return res.status(400).send({ error: 'ValidationError' }); } });

ขอให้สังเกตว่าฉันใช้ ES6 Object Shorthand เพื่อส่งคืนวัตถุ book ทันทีกลับไปยังลูกค้าในกรณีที่ประสบความสำเร็จด้วย res.send({ book }) - ซึ่งจะเทียบเท่ากับ res.send({ book: book }) ฉันยังส่งคืนนิพจน์เพียงเพื่อให้แน่ใจว่าฟังก์ชันของฉันออก ในบล็อก catch ฉันตั้งค่าสถานะเป็น 400 อย่างชัดเจน และส่งคืนสตริง 'ValidationError' ในคุณสมบัติ error ของวัตถุที่ส่งกลับ 201 คือรหัสสถานะเส้นทางความสำเร็จซึ่งหมายถึง "สร้างแล้ว"

อันที่จริง นี่ไม่ใช่ทางออกที่ดีที่สุดเพราะเราไม่แน่ใจจริงๆ ว่าสาเหตุของความล้มเหลวคือคำขอที่ไม่ดีจากฝั่งไคลเอ็นต์ บางทีเราอาจสูญเสียการเชื่อมต่อ (หากการเชื่อมต่อซ็อกเก็ตหลุด ดังนั้นจึงเป็นข้อยกเว้นชั่วคราว) กับฐานข้อมูล ซึ่งในกรณีนี้ เราน่าจะส่งคืนข้อผิดพลาด 500 Internal Server วิธีตรวจสอบสิ่งนี้คืออ่านอ็อบเจกต์ข้อผิดพลาด e และเลือกส่งคืนการตอบสนอง มาทำกันตอนนี้ แต่อย่างที่ฉันพูดไปหลายครั้งแล้ว บทความติดตามจะกล่าวถึงสถาปัตยกรรมที่เหมาะสมในแง่ของเราเตอร์ ตัวควบคุม บริการ ที่เก็บ คลาสข้อผิดพลาดที่กำหนดเอง มิดเดิลแวร์ข้อผิดพลาดที่กำหนดเอง การตอบกลับข้อผิดพลาดที่กำหนดเอง ข้อมูลโมเดลฐานข้อมูล/เอนทิตีของโดเมน การทำแผนที่และการแยกข้อความค้นหาคำสั่ง (CQS)

 app.post('/books', async (req, res) => { try { const book = new Book(req.body.book); await book.save(); return res.send({ book }); } catch (e) { if (e instanceof mongoose.Error.ValidationError) { return res.status(400).send({ error: 'ValidationError' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

ไปข้างหน้าและเปิดบุรุษไปรษณีย์ (สมมติว่าคุณมีมิฉะนั้นให้ดาวน์โหลดและติดตั้ง) และสร้างคำขอใหม่ เราจะทำการส่งคำขอ POST ไปยัง localhost:3000/books ใต้แท็บ "ร่างกาย" ในส่วนคำขอของบุรุษไปรษณีย์ ฉันจะเลือกปุ่มตัวเลือก "ดิบ" และเลือก "JSON" ในปุ่มแบบเลื่อนลงทางด้านขวาสุด การดำเนินการนี้จะดำเนินต่อไปและเพิ่มส่วนหัว Content-Type: application/json ไปยังคำขอโดยอัตโนมัติ จากนั้นฉันจะคัดลอกและวาง Book JSON Object จากก่อนหน้านี้ลงในพื้นที่ข้อความเนื้อหา นี่คือสิ่งที่เรามี:

Postman GUI เติมด้วยข้อมูลการตอบสนองจากคำขอ POST
JSON Payload ตอบสนองต่อคำขอ POST ของเรา (ตัวอย่างขนาดใหญ่)

หลังจากนั้น ฉันจะกดปุ่มส่ง และคุณควรเห็นการตอบกลับที่สร้าง 201 รายการในส่วน "การตอบสนอง" ของบุรุษไปรษณีย์ (แถวล่างสุด) เราเห็นสิ่งนี้เนื่องจากเราขอให้ Express ตอบกลับด้วย 201 และวัตถุ Book โดยเฉพาะ หากเราเพิ่งทำ res.send() โดยไม่มีรหัสสถานะ express ก็จะตอบกลับด้วย 200 OK โดยอัตโนมัติ อย่างที่คุณเห็น ตอนนี้วัตถุ Book ถูกบันทึกลงในฐานข้อมูลและได้ส่งคืนไปยังไคลเอนต์เป็นการตอบสนองต่อคำขอ POST

Postman GUI เติมข้อมูลสำหรับคำขอ POST
ข้อมูลเพื่อเติมฟิลด์บุรุษไปรษณีย์ด้วยสำหรับคำขอ POST ของเรา (ตัวอย่างขนาดใหญ่)

หากคุณดูฐานข้อมูล Book Collection ผ่าน MongoDB Atlas คุณจะเห็นว่าหนังสือเล่มนี้ได้รับการบันทึกจริงๆ

คุณยังสามารถบอกได้ว่า MongoDB ได้แทรก __v และ _id อดีตแสดงถึงเวอร์ชันของเอกสาร ในกรณีนี้คือ 0 และอันหลังคือ ObjectID ของเอกสาร ซึ่ง MongoDB สร้างขึ้นโดยอัตโนมัติและรับประกันว่ามีโอกาสเกิดการชนกันต่ำ

บทสรุปของสิ่งที่เราได้กล่าวถึงจนถึงตอนนี้

เราได้กล่าวถึงเรื่องนี้มากมายในบทความ มาดูการบรรเทาโทษสั้นๆ โดยดูสรุปสั้นๆ ก่อนกลับไปเพื่อจบ Express API

เราได้เรียนรู้เกี่ยวกับ ES6 Object Destructuring, ES6 Object Shorthand Syntax, เช่นเดียวกับตัวดำเนินการ ES6 Rest/Spread ทั้งสามสิ่งนี้ให้เราทำสิ่งต่อไปนี้ (และอื่น ๆ ตามที่กล่าวไว้ข้างต้น):

 // Destructuring Object Properties: const { a: newNameA = 'Default', b } = { a: 'someData', b: 'info' }; console.log(`newNameA: ${newNameA}, b: ${b}`); // newNameA: someData, b: info // Destructuring Array Elements const [elemOne, elemTwo] = [() => console.log('hi'), 'data']; console.log(`elemOne(): ${elemOne()}, elemTwo: ${elemTwo}`); // elemOne(): hi, elemTwo: data // Object Shorthand const makeObj = (name) => ({ name }); console.log(`makeObj('Tim'): ${JSON.stringify(makeObj('Tim'))}`); // makeObj('Tim'): { "name": "Tim" } // Rest, Spread const [c, d, ...rest] = [0, 1, 2, 3, 4]; console.log(`c: ${c}, d: ${d}, rest: ${rest}`) // c: 0, d: 1, rest: 2, 3, 4

นอกจากนี้เรายังครอบคลุม Express, Expess Middleware, Servers, Ports, IP Addressing เป็นต้น สิ่งต่างๆ ได้น่าสนใจเมื่อเราได้เรียนรู้ว่ามีวิธีที่มีอยู่ในผลลัพธ์การส่งคืนจาก require('express')(); ด้วยชื่อของ HTTP Verbs เช่น app.get และ app.post

หากส่วนที่ require('express')() ไม่สมเหตุสมผลสำหรับคุณ นี่คือสิ่งที่ฉันกำลังทำ:

 const express = require('express'); const app = express(); app.someHTTPVerb

มันควรจะสมเหตุสมผลในแบบเดียวกับที่เราเลิกกิจการโรงงานเชื่อมต่อมาก่อนสำหรับพังพอน

ตัวจัดการเส้นทางแต่ละตัว ซึ่งเป็นฟังก์ชันจุดปลาย (หรือฟังก์ชันเรียกกลับ) ถูกส่งผ่านในวัตถุ req และวัตถุ res จาก Express เบื้องหลัง (ในทางเทคนิคแล้วพวกเขาจะได้รับ next อย่างที่เราเห็นในไม่กี่นาที) req มีข้อมูลเฉพาะสำหรับคำขอที่เข้ามาจากลูกค้า เช่น ส่วนหัวหรือ JSON ที่ส่ง res คือสิ่งที่ทำให้เราตอบกลับลูกค้าได้ ฟังก์ชัน next จะถูกส่งไปยังตัวจัดการด้วย

ด้วย Mongoose เราได้เห็นวิธีที่เราสามารถเชื่อมต่อกับฐานข้อมูลด้วยสองวิธี — วิธีดั้งเดิมและวิธีที่ล้ำหน้า/ใช้งานได้จริงที่ยืมมาจากรูปแบบโรงงาน เราจะใช้สิ่งนี้เมื่อเราพูดถึงการทดสอบหน่วยและการรวมกับ Jest (และการทดสอบการกลายพันธุ์) เพราะจะอนุญาตให้เราสร้างอินสแตนซ์การทดสอบของ DB ที่มีข้อมูลเริ่มต้นซึ่งเราสามารถเรียกใช้การยืนยันได้

หลังจากนั้น เราสร้างอ็อบเจ็กต์ Mongoose schema และใช้เพื่อสร้างโมเดล และจากนั้นเรียนรู้วิธีที่เราสามารถเรียก Constructor ของโมเดลนั้นเพื่อสร้างอินสแตนซ์ใหม่ได้ ที่มีอยู่ในอินสแตนซ์คือวิธีการ save (รวมถึงวิธีอื่นๆ) ซึ่งมีลักษณะไม่ตรงกัน และจะตรวจสอบว่าโครงสร้างวัตถุที่เราส่งผ่านนั้นสอดคล้องกับสคีมา แก้ไขสัญญาหากมี และปฏิเสธสัญญาด้วย ValidationError หาก มันไม่ใช่. ในกรณีของการแก้ไข เอกสารใหม่จะถูกบันทึกลงในฐานข้อมูล และเราตอบกลับด้วย HTTP 200 OK/201 CREATED ไม่เช่นนั้น เราจะตรวจพบข้อผิดพลาดที่เกิดขึ้นในปลายทางของเรา และส่งคืน HTTP 400 Bad Request ไปยังไคลเอนต์

ในขณะที่คุณสร้างปลายทางของเราต่อไป คุณจะได้เรียนรู้เพิ่มเติมเกี่ยวกับวิธีการบางอย่างที่มีในโมเดลและอินสแตนซ์ของโมเดล

สิ้นสุดปลายทางของเรา

หลังจากเสร็จสิ้น POST Endpoint แล้ว มาจัดการ GET กัน ดังที่ฉันได้กล่าวไว้ก่อนหน้านี้ :id syntax ภายในเส้นทางทำให้ Express รู้ว่า id เป็นพารามิเตอร์ของเส้นทาง ซึ่งสามารถเข้าถึงได้จาก req.params คุณเห็นแล้วว่าเมื่อคุณจับคู่ ID บางอย่างสำหรับพารามิเตอร์ "ตัวแทน" ในเส้นทาง จะมีการพิมพ์ไปยังหน้าจอในตัวอย่างแรกๆ ตัวอย่างเช่น หากคุณส่งคำขอ GET ไปที่ “/books/test-id-123” ดังนั้น req.params.id จะเป็นสตริง test-id-123 เนื่องจากชื่อพารามิเตอร์เป็น id โดยมีเส้นทางเป็น HTTP GET /books/:id .

สิ่งที่เราต้องทำคือดึง ID นั้นจากวัตถุ req และตรวจดูว่าเอกสารใดในฐานข้อมูลของเรามี ID เดียวกันหรือไม่ ซึ่ง Mongoose (และ Native Driver) นั้นทำได้ง่ายมาก

 app.get('/books/:id', async (req, res) => { const book = await Book.findById(req.params.id); console.log(book); res.send({ book }); });

คุณจะเห็นว่าสามารถเข้าถึงได้จากโมเดลของเราเป็นฟังก์ชันที่เราเรียกได้ซึ่งจะค้นหาเอกสารโดยใช้ ID เบื้องหลัง Mongoose จะส่ง ID ใดก็ตามที่เราส่งไปยัง findById เป็นประเภทของฟิลด์ _id ในเอกสาร หรือในกรณีนี้ ObjectId หากพบ ID ที่ตรงกัน (และจะพบเพียงรหัสเดียวสำหรับ ObjectId ที่มีความน่าจะเป็นการชนกันที่ต่ำมาก) เอกสารนั้นจะถูกวางไว้ในตัวแปรค่าคงที่ book ของเรา มิฉะนั้น book จะถือเป็นโมฆะ — ความจริงที่เราจะใช้ในอนาคตอันใกล้นี้

ในตอนนี้ ให้เริ่มเซิร์ฟเวอร์ใหม่ (คุณต้องเริ่มเซิร์ฟเวอร์ใหม่ เว้นแต่ว่าคุณกำลังใช้ nodemon ) และตรวจดูให้แน่ใจว่าเรายังมีเอกสารหนังสือเล่มเดียวจากก่อนหน้านี้ในคอลเลคชัน Books ไปข้างหน้าและคัดลอก ID ของเอกสารนั้นซึ่งเป็นส่วนที่ไฮไลต์ของรูปภาพด้านล่าง:

เอกสารหนังสือ ObjectID
ตัวอย่างของ ObjectID ที่จะใช้สำหรับคำขอ GET ที่กำลังจะมีขึ้น (ตัวอย่างขนาดใหญ่)

และใช้มันเพื่อขอ GET ไปยัง /books/:id กับบุรุษไปรษณีย์ ดังนี้ (โปรดทราบว่าข้อมูลร่างกายเพิ่งจะเหลือจากคำขอ POST ก่อนหน้าของฉัน มันไม่ได้ถูกใช้จริง ๆ แม้ว่าจะปรากฎในภาพด้านล่างก็ตาม) :

บุรุษไปรษณีย์ GUI ที่มีข้อมูลสำหรับคำขอ GET
API URL และข้อมูลบุรุษไปรษณีย์สำหรับคำขอ GET (ตัวอย่างขนาดใหญ่)

เมื่อทำเช่นนั้น คุณควรนำเอกสารหนังสือที่มี ID ที่ระบุกลับมาอยู่ในส่วนการตอบกลับของบุรุษไปรษณีย์ โปรดสังเกตว่า ก่อนหน้านี้ ด้วยเส้นทาง POST ซึ่งออกแบบมาเพื่อ "POST" หรือ "พุช" ทรัพยากรใหม่ไปยังเซิร์ฟเวอร์ เราตอบกลับด้วยการสร้าง 201 รายการ — เนื่องจากมีการสร้างทรัพยากร (หรือเอกสาร) ใหม่ ในกรณีของ GET ไม่มีอะไรใหม่เกิดขึ้น — เราเพิ่งขอทรัพยากรที่มี ID เฉพาะ ดังนั้นรหัสสถานะ 200 OK คือสิ่งที่เราได้รับกลับมา แทนที่จะเป็น 201 สร้าง

ตามปกติในด้านการพัฒนาซอฟต์แวร์ จะต้องคำนึงถึงกรณีของ Edge — การป้อนข้อมูลของผู้ใช้นั้นไม่ปลอดภัยและผิดพลาดโดยเนื้อแท้ และเป็นหน้าที่ของเรา ในฐานะนักพัฒนา ที่จะต้องมีความยืดหยุ่นกับประเภทของอินพุตที่เราจะได้รับและตอบสนองต่อพวกเขา ตามนั้น เราจะทำอย่างไรถ้าผู้ใช้ (หรือผู้เรียก API) ส่ง ID บางอย่างที่ไม่สามารถส่งไปยัง MongoDB ObjectID หรือ ID ที่สามารถส่งได้ แต่ไม่มีอยู่จริง

สำหรับกรณีก่อนหน้านี้ Mongoose จะโยน CastError ซึ่งเข้าใจได้เพราะถ้าเราระบุ ID เช่น math-is-fun นั่นก็ไม่ใช่สิ่งที่สามารถแคสต์ไปยัง ObjectID ได้ และการแคสต์ไปยัง ObjectID นั้นเป็นสิ่งที่เฉพาะเจาะจง Mongoose กำลังทำภายใต้ประทุน

สำหรับกรณีหลัง เราสามารถแก้ไขปัญหาได้อย่างง่ายดายผ่าน Null Check หรือ Guard Clause ไม่ว่าจะด้วยวิธีใด ฉันจะส่งกลับและ HTTP 404 Not Found Response ผมจะแสดงให้คุณเห็นสองสามวิธีที่เราทำได้ ทั้งในทางที่ไม่ดี และในทางที่ดีขึ้น

ประการแรก เราสามารถทำสิ่งต่อไปนี้:

 app.get('/books/:id', async (req, res) => { try { const book = await Book.findById(req.params.id); if (!book) throw new Error(); return res.send({ book }); } catch (e) { return res.status(404).send({ error: 'Not Found' }); } });

ใช้งานได้และเราสามารถใช้งานได้ดี ฉันคาดว่าคำสั่งที่ await Book.findById() จะโยน Mongoose CastError หากไม่สามารถส่งสตริง ID ไปยัง ObjectID ได้ ทำให้บล็อก catch ทำงาน หากสามารถแคสต์ได้ แต่ไม่มี ObjectID ที่สอดคล้องกัน book จะเป็น null และการตรวจสอบค่าว่างจะส่งข้อผิดพลาด โดยจะเริ่มต้นบล็อก catch อีกครั้ง Inside catch เราเพิ่งส่งคืน 404 มีปัญหาสองประการที่นี่ ขั้นแรก แม้ว่าหนังสือจะพบแต่เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุอื่นๆ เราจะส่ง 404 กลับเมื่อเราน่าจะให้ลูกค้ารับทั้งหมด 500 ฉบับ ประการที่สอง เราไม่ได้แยกแยะจริงๆ ว่า ID ที่ส่งไปนั้นถูกต้องหรือไม่ แต่ ไม่มีอยู่จริงหรือว่าเป็นเพียง ID ที่ไม่ถูกต้อง

นี่เป็นอีกวิธีหนึ่ง:

 const mongoose = require('mongoose'); app.get('/books/:id', async (req, res) => { try { const book = await Book.findById(req.params.id); if (!book) return res.status(404).send({ error: 'Not Found' }); return res.send({ book }); } catch (e) { if (e instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

สิ่งที่ดีเกี่ยวกับสิ่งนี้คือเราสามารถจัดการทั้งสามกรณีของ 400, 404 และ 500 ทั่วไป โปรดสังเกตว่าหลังจาก Null Check on book ฉันใช้คีย์เวิร์ด return ในการตอบสนองของฉัน สิ่งนี้สำคัญมากเพราะเราต้องการให้แน่ใจว่าเราออกจากตัวจัดการเส้นทางที่นั่น

อาจมีทางเลือกอื่นให้เราตรวจสอบว่า id บน req.params สามารถส่งไปยัง ObjectID ได้อย่างชัดเจนหรือไม่ เมื่อเทียบกับการอนุญาตให้ Mongoose ส่งโดยปริยายด้วย mongoose.Types.ObjectId.isValid('id); แต่มีกรณีขอบที่มีสตริง 12 ไบต์ที่ทำให้บางครั้งทำงานโดยไม่คาดคิด

เราอาจทำให้การกล่าวซ้ำเจ็บปวดน้อยลงด้วย Boom เช่น ไลบรารี HTTP Response หรือเราอาจใช้ Error Handling Middleware เรายังสามารถเปลี่ยน Mongoose Errors ให้อ่านได้ง่ายขึ้นด้วย Mongoose Hooks/Middleware ดังที่อธิบายไว้ที่นี่ ตัวเลือกเพิ่มเติมคือการกำหนดออบเจ็กต์ข้อผิดพลาดที่กำหนดเองและใช้ Global Express Error Handling Middleware อย่างไรก็ตาม ฉันจะบันทึกไว้สำหรับบทความที่กำลังจะถึงนี้ ซึ่งเราจะพูดถึงวิธีการทางสถาปัตยกรรมที่ดีขึ้น

ในจุดสิ้นสุดสำหรับ PATCH /books/:id เราคาดว่าอ็อบเจ็กต์การอัปเดตจะถูกส่งต่อซึ่งมีการอัปเดตสำหรับหนังสือที่เป็นปัญหา สำหรับบทความนี้ เราจะอนุญาตให้อัปเดตทุกช่อง แต่ในอนาคต ผมจะแสดงให้เห็นว่าเราจะไม่อนุญาตให้อัปเดตบางฟิลด์ได้อย่างไร นอกจากนี้ คุณจะเห็นว่าตรรกะการจัดการข้อผิดพลาดใน PATCH Endpoint จะเหมือนกับ GET Endpoint ของเรา นั่นเป็นสัญญาณว่าเรากำลังละเมิดหลักการของ DRY แต่เราจะพูดถึงเรื่องนี้อีกครั้งในภายหลัง

ฉันคาดหวังว่าการอัปเดตทั้งหมดจะมีอยู่ในวัตถุ updates ของ req.body (หมายความว่าลูกค้าจะส่ง JSON ที่มีอ็อบเจ็กต์ updates ) และจะใช้ฟังก์ชัน Book.findByAndUpdate พร้อมแฟล็กพิเศษเพื่อดำเนินการอัปเดต

 app.patch('/books/:id', async (req, res) => { const { id } = req.params; const { updates } = req.body; try { const updatedBook = await Book.findByIdAndUpdate(id, updates, { runValidators: true, new: true }); if (!updatedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: updatedBook }); } catch (e) { if (e instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

สังเกตบางสิ่งที่นี่ ก่อนอื่นเราจะทำลาย id จาก req.params และ updates จาก req.body

มีให้ในรุ่น Book เป็นฟังก์ชันที่ใช้ชื่อ findByIdAndUpdate ที่ใช้ ID ของเอกสารที่เป็นปัญหา การอัปเดตเพื่อดำเนินการ และออบเจ็กต์ตัวเลือกเสริม โดยปกติ Mongoose จะไม่ทำการตรวจสอบความถูกต้องอีกครั้งสำหรับการดำเนินการอัปเดต ดังนั้น runValidators: true เราส่งผ่านเนื่องจากอ็อบเจกต์ options บังคับให้ทำเช่นนั้น นอกจากนี้ สำหรับ Mongoose 4 Model.findByIdAndUpdate จะไม่ส่งคืนเอกสารที่แก้ไขแล้ว แต่จะส่งคืนเอกสารต้นฉบับแทน แฟล็ก new: true (ซึ่งเป็นค่าดีฟอลต์เป็นเท็จ) จะแทนที่พฤติกรรมนั้น

สุดท้าย เราสามารถสร้างปลายทาง DELETE ของเรา ซึ่งค่อนข้างคล้ายกับจุดปลายอื่นๆ ทั้งหมด:

 app.delete('/books/:id', async (req, res) => { try { const deletedBook = await Book.findByIdAndDelete(req.params.id); if (!deletedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: deletedBook }); } catch (e) { if (e instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

ด้วยเหตุนี้ API ดั้งเดิมของเราจึงสมบูรณ์ และคุณสามารถทดสอบได้โดยส่งคำขอ HTTP ไปยังปลายทางทั้งหมด

ข้อจำกัดความรับผิดชอบสั้น ๆ เกี่ยวกับสถาปัตยกรรมและเราจะแก้ไขอย่างไร

จากมุมมองทางสถาปัตยกรรม โค้ดที่เรามีในที่นี้ค่อนข้างแย่ ยุ่งเหยิง มันไม่แห้ง มันไม่ใช่ SOLID จริงๆ แล้ว คุณอาจจะเรียกว่าน่ารังเกียจด้วยซ้ำ สิ่งที่เรียกว่า “ตัวจัดการเส้นทาง” เหล่านี้ทำมากกว่าแค่ “การส่งต่อเส้นทาง” — พวกเขากำลังเชื่อมต่อโดยตรงกับฐานข้อมูลของเรา หมายความว่าไม่มีสิ่งที่เป็นนามธรรมโดยสิ้นเชิง

มาเถอะ แอปพลิเคชั่นส่วนใหญ่จะไม่มีขนาดเล็กขนาดนี้ หรือคุณอาจหลีกเลี่ยงสถาปัตยกรรมแบบไร้เซิร์ฟเวอร์ด้วยฐานข้อมูล Firebase อย่างที่เราเห็นในภายหลัง ผู้ใช้ต้องการความสามารถในการอัปโหลดรูปแทนตัว คำพูด และตัวอย่างจากหนังสือของพวกเขา ฯลฯ บางทีเราอาจต้องการเพิ่มคุณสมบัติการแชทสดระหว่างผู้ใช้ที่มี WebSockets และลองพูดกันว่าเรา จะเปิดแอปพลิเคชันของเราเพื่อให้ผู้ใช้ยืมหนังสือร่วมกันโดยมีค่าใช้จ่ายเล็กน้อย ณ จุดนั้น เราจำเป็นต้องพิจารณาการรวมการชำระเงินกับ Stripe API และการขนส่งทางเรือด้วย Shippo API

สมมติว่าเราดำเนินการกับสถาปัตยกรรมปัจจุบันของเราและเพิ่มฟังก์ชันทั้งหมดนี้ ตัวจัดการเส้นทางเหล่านี้หรือที่รู้จักในชื่อ Controller Actions นั้นจะมีขนาดใหญ่มาก และมี ความซับซ้อนแบบไซโค ลมาติกสูง รูปแบบการเข้ารหัสดังกล่าวอาจเหมาะกับเราในช่วงแรกๆ แต่ถ้าเราตัดสินใจว่าข้อมูลของเราเป็นข้อมูลอ้างอิงและ PostgreSQL เป็นตัวเลือกฐานข้อมูลที่ดีกว่า MongoDB ล่ะ ตอนนี้เราต้องปรับโครงสร้างแอปพลิเคชันทั้งหมดของเราใหม่ ลอก Mongoose ออก ปรับเปลี่ยนตัวควบคุมของเรา ฯลฯ ซึ่งทั้งหมดนี้อาจนำไปสู่ข้อบกพร่องที่อาจเกิดขึ้นในส่วนที่เหลือของตรรกะทางธุรกิจ อีกตัวอย่างหนึ่งคือการตัดสินใจว่า AWS S3 มีราคาแพงเกินไป และเราต้องการย้ายไปยัง GCP ย้ำอีกครั้งว่าต้องมีการปรับโครงสร้างใหม่ทั้งแอปพลิเคชัน

แม้ว่าจะมีความคิดเห็นมากมายเกี่ยวกับสถาปัตยกรรม ตั้งแต่การออกแบบที่ขับเคลื่อนด้วยโดเมน การแยกความรับผิดชอบในการสืบค้นคำสั่ง และการจัดหาเหตุการณ์ ไปจนถึงการพัฒนาที่ขับเคลื่อนด้วยการทดสอบ SOILD สถาปัตยกรรมแบบเลเยอร์ สถาปัตยกรรม Onion และอื่นๆ เราจะเน้นการนำสถาปัตยกรรมแบบเลเยอร์อย่างง่ายมาใช้ใน บทความในอนาคตประกอบด้วย Controllers, Services และ Repositories และการใช้ Design Patterns เช่น Composition, Adapters/Wrappers และ Inversion of Control via Dependency Injection แม้ว่าในระดับหนึ่ง สิ่งนี้สามารถทำได้บ้างด้วย JavaScript เราจะพิจารณาตัวเลือก TypeScript เพื่อให้ได้สถาปัตยกรรมนี้เช่นกัน โดยอนุญาตให้เราใช้กระบวนทัศน์การเขียนโปรแกรมเชิงฟังก์ชัน เช่น Monads ทั้งสอง นอกเหนือจากแนวคิด OOP เช่น Generics

สำหรับตอนนี้ มีการเปลี่ยนแปลงเล็กน้อยสองอย่างที่เราสามารถทำได้ เนื่องจากตรรกะการจัดการข้อผิดพลาดของเราค่อนข้างคล้ายกันในบล็อกการ catch ของปลายทางทั้งหมด เราจึงสามารถแยกไปยังฟังก์ชัน Express Error Handling Middleware แบบกำหนดเองที่ส่วนท้ายสุดของสแต็กได้

ทำความสะอาดสถาปัตยกรรมของเรา

ในปัจจุบัน เรากำลังทำซ้ำตรรกะการจัดการข้อผิดพลาดจำนวนมากในทุกจุดสิ้นสุดของเรา แต่เราสามารถสร้างฟังก์ชัน Express Error Handling Middleware ซึ่งเป็นฟังก์ชัน Express Middleware ที่ได้รับการเรียกโดยมีข้อผิดพลาด ออบเจ็กต์ req และ res และฟังก์ชันถัดไป

สำหรับตอนนี้ มาสร้างฟังก์ชันมิดเดิลแวร์กันก่อน สิ่งที่ฉันจะทำคือทำซ้ำตรรกะการจัดการข้อผิดพลาดเดียวกันกับที่เราคุ้นเคย:

 app.use((err, req, res, next) => { if (err instanceof mongoose.Error.ValidationError) { return res.status(400).send({ error: 'Validation Error' }); } else if (err instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { console.log(err); // Unexpected, so worth logging. return res.status(500).send({ error: 'Internal error' }); } });

ดูเหมือนว่าจะใช้งานไม่ได้กับ Mongoose Errors แต่โดยทั่วไป แทนที่จะใช้ if/else if/else เพื่อกำหนดอินสแตนซ์ของข้อผิดพลาด คุณสามารถสลับไปยัง Constructor ของข้อผิดพลาดได้ ฉันจะทิ้งสิ่งที่เรามี

ในตัวจัดการปลายทาง/เส้นทางแบบ ซิงโครนั ส หากคุณเกิดข้อผิดพลาด Express จะตรวจจับและประมวลผลโดยที่คุณไม่ต้องดำเนินการใดๆ เพิ่มเติม น่าเสียดายที่ไม่ใช่กรณีของเรา เรากำลังติดต่อกับรหัส อะซิงโครนัส ในการมอบหมายการจัดการข้อผิดพลาดให้กับ Express ด้วยตัวจัดการเส้นทางแบบ async เรามักจะตรวจจับข้อผิดพลาดด้วยตนเองและส่งผ่านไปยัง next()

ดังนั้น ฉันจะอนุญาตให้ next เป็นอาร์กิวเมนต์ที่สามในจุดสิ้นสุด และฉันจะลบตรรกะการจัดการข้อผิดพลาดในบล็อก catch แทนที่จะส่งอินสแตนซ์ข้อผิดพลาดไปที่ next เช่นนี้

 app.post('/books', async (req, res, next) => { try { const book = new Book(req.body.book); await book.save(); return res.send({ book }); } catch (e) { next(e) } });

หากคุณทำเช่นนี้กับตัวจัดการเส้นทางทั้งหมด คุณควรลงเอยด้วยรหัสต่อไปนี้:

 const express = require('express'); const mongoose = require('mongoose'); // Database connection and model. require('./db/mongoose.js')(); const Book = require('./models/book.js'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // HTTP POST /books app.post('/books', async (req, res, next) => { try { const book = new Book(req.body.book); await book.save(); return res.status(201).send({ book }); } catch (e) { next(e) } }); // HTTP GET /books/:id app.get('/books/:id', async (req, res) => { try { const book = await Book.findById(req.params.id); if (!book) return res.status(404).send({ error: 'Not Found' }); return res.send({ book }); } catch (e) { next(e); } }); // HTTP PATCH /books/:id app.patch('/books/:id', async (req, res, next) => { const { id } = req.params; const { updates } = req.body; try { const updatedBook = await Book.findByIdAndUpdate(id, updates, { runValidators: true, new: true }); if (!updatedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: updatedBook }); } catch (e) { next(e); } }); // HTTP DELETE /books/:id app.delete('/books/:id', async (req, res, next) => { try { const deletedBook = await Book.findByIdAndDelete(req.params.id); if (!deletedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: deletedBook }); } catch (e) { next(e); } }); // Notice - bottom of stack. app.use((err, req, res, next) => { if (err instanceof mongoose.Error.ValidationError) { return res.status(400).send({ error: 'Validation Error' }); } else if (err instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { console.log(err); // Unexpected, so worth logging. return res.status(500).send({ error: 'Internal error' }); } }); // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

การย้ายต่อไป การแยกข้อผิดพลาดในการจัดการมิดเดิลแวร์ของเราออกเป็นไฟล์อื่นเป็นเรื่องที่คุ้มค่า แต่นั่นก็ไม่สำคัญ และเราจะได้เห็นมันในบทความต่อๆ ไปในชุดนี้ นอกจากนี้ เราสามารถใช้โมดูล NPM ที่ชื่อ express-async-errors เพื่ออนุญาตให้เราไม่ต้องโทรถัดไปในบล็อก catch แต่อีกครั้ง ฉันกำลังพยายามแสดงให้คุณเห็นถึงวิธีการดำเนินการต่างๆ อย่างเป็นทางการ

คำเกี่ยวกับ CORS และนโยบายต้นกำเนิดเดียวกัน

สมมติว่าเว็บไซต์ของคุณให้บริการจากโดเมน myWebsite.com แต่เซิร์ฟเวอร์ของคุณอยู่ที่ myOtherDomain.com/api CORS ย่อมาจาก Cross-Origin Resource Sharing และเป็นกลไกที่สามารถทำการร้องขอข้ามโดเมนได้ ในกรณีข้างต้น เนื่องจากเซิร์ฟเวอร์และโค้ด JS ส่วนหน้าอยู่ในโดเมนที่ต่างกัน คุณจะต้องส่งคำขอจากแหล่งที่มาที่แตกต่างกันสองแห่ง ซึ่งโดยทั่วไปแล้วเบราว์เซอร์จะจำกัดด้วยเหตุผลด้านความปลอดภัย และบรรเทาได้ด้วยการจัดหาส่วนหัว HTTP ที่เฉพาะเจาะจง

นโยบายต้นกำเนิดเดียวกันคือสิ่งที่ปฏิบัติตามข้อจำกัดดังกล่าว — เว็บเบราว์เซอร์จะอนุญาตให้ต้องทำข้ามแหล่งกำเนิดเดียวกันเท่านั้น

เราจะพูดถึง CORS และ SOP ในภายหลังเมื่อเราสร้าง Webpack ที่รวม front-end สำหรับ Book API ของเราด้วย React

บทสรุปและอะไรต่อไป

เราได้พูดคุยกันมากมายในบทความนี้ บางทีอาจใช้ไม่ได้ทั้งหมด แต่หวังว่าคุณจะสะดวกมากขึ้นในการทำงานกับคุณลักษณะ Express และ ES6 JavaScript หากคุณยังใหม่ต่อการเขียนโปรแกรมและ Node เป็นเส้นทางแรกที่คุณกำลังลงมือ หวังว่าการอ้างอิงถึงภาษาประเภทสแตติก เช่น Java, C++ และ C# จะช่วยเน้นความแตกต่างระหว่าง JavaScript และคู่แบบคงที่

Next time, we'll finish building out our Book API by making some fixes to our current setup with regards to the Book Routes, as well as adding in User Authentication so that users can own books. We'll do all of this with a similar architecture to what I described here and with MongoDB for data persistence. Finally, we'll permit users to upload avatar images to AWS S3 via Buffers.

In the article thereafter, we'll be rebuilding our application from the ground up in TypeScript, still with Express. We'll also move to PostgreSQL with Knex instead of MongoDB with Mongoose as to depict better architectural practices. Finally, we'll update our avatar image uploading process to use Node Streams (we'll discuss Writable, Readable, Duplex, and Transform Streams). Along the way, we'll cover a great amount of design and architectural patterns and functional paradigms, including:

  • Controllers/Controller Actions
  • บริการ
  • ที่เก็บ
  • Data Mapping
  • The Adapter Pattern
  • The Factory Pattern
  • The Delegation Pattern
  • OOP Principles and Composition vs Inheritance
  • Inversion of Control via Dependency Injection
  • SOLID Principles
  • Coding against interfaces
  • Data Transfer Objects
  • Domain Models and Domain Entities
  • Either Monads
  • การตรวจสอบความถูกต้อง
  • Decorators
  • Logging and Logging Levels
  • Unit Tests, Integration Tests (E2E), and Mutation Tests
  • The Structured Query Language
  • ความสัมพันธ์
  • HTTP/Express Security Best Practices
  • Node Best Practices
  • OWASP Security Best Practices
  • และอื่น ๆ.

Using that new architecture, in the article after that, we'll write Unit, Integration, and Mutation tests, aiming for close to 100 percent testing coverage, and we'll finally discuss setting up a remote CI/CD pipeline with CircleCI, as well as Message Busses, Job/Task Scheduling, and load balancing/reverse proxying.

Hopefully, this article has been helpful, and if you have any queries or concerns, let me know in the comments below.