الشروع في العمل مع Express و ES6 + JavaScript Stack
نشرت: 2022-03-10هذه المقالة هي الجزء الثاني من سلسلة ، مع الجزء الأول الموجود هنا ، والذي قدم نظرة أساسية (ونأمل أن تكون) بديهية حول Node.js و ES6 + JavaScript ووظائف رد الاتصال ووظائف الأسهم وواجهات برمجة التطبيقات وبروتوكول HTTP و JSON و MongoDB و أكثر.
في هذه المقالة ، سنبني على المهارات التي اكتسبناها في السابقة ، وتعلم كيفية تنفيذ ونشر قاعدة بيانات MongoDB لتخزين معلومات قائمة كتب المستخدم ، وبناء واجهة برمجة تطبيقات مع Node.js وإطار عمل تطبيق الويب السريع لفضح قاعدة البيانات هذه وأداء عمليات CRUD عليه ، وأكثر من ذلك. على طول الطريق ، سنناقش ES6 Object Destructuring ، و ES6 Object Shorthand ، و Async / Await syntax ، و Spread Operator ، وسنلقي نظرة سريعة على CORS ، وسياسة الأصل نفسها ، والمزيد.
في مقال لاحق ، سنقوم بإعادة تشكيل قاعدة الكود الخاصة بنا فيما يتعلق بفصل المخاوف من خلال استخدام بنية ثلاثية الطبقات وتحقيق انعكاس التحكم عبر حقن التبعية ، وسنقوم بتنفيذ JSON Web Token و Firebase Authentication المستند إلى الأمان والتحكم في الوصول ، وتعلم كيفية الأمان بشكل آمن تخزين كلمات المرور ، واستخدام خدمة التخزين البسيط من AWS لتخزين الصور الرمزية للمستخدم باستخدام المخازن المؤقتة والتدفقات Node.js - كل ذلك أثناء استخدام PostgreSQL لاستمرار البيانات. على طول الطريق ، سنعيد كتابة قاعدة الكود الخاصة بنا من الألف إلى الياء في TypeScript لفحص مفاهيم OOP الكلاسيكية (مثل تعدد الأشكال والوراثة والتكوين وما إلى ذلك) وحتى أنماط التصميم مثل المصانع والمحولات.
كلمة للتحذير
هناك مشكلة في غالبية المقالات التي تناقش Node.js المتوفرة اليوم. معظمهم ، وليس جميعهم ، لا يذهبون إلى أبعد من وصف كيفية إعداد Express Routing ، ودمج Mongoose ، وربما استخدام مصادقة JSON Web Token. تكمن المشكلة في أنهم لا يتحدثون عن الهندسة المعمارية ، أو أفضل الممارسات الأمنية ، أو عن مبادئ الترميز النظيف ، أو امتثال ACID ، أو قواعد البيانات العلائقية ، أو النموذج العادي الخامس ، أو نظرية CAP أو المعاملات. إما أنه من المفترض أنك تعرف كل ما سيأتي ، أو أنك لن تبني مشاريع كبيرة أو شائعة بما يكفي لتضمن تلك المعرفة المذكورة أعلاه.
يبدو أن هناك عدة أنواع مختلفة من مطوري Node - من بين آخرين ، بعضها جديد في البرمجة بشكل عام ، والبعض الآخر يأتي من تاريخ طويل من تطوير المؤسسات باستخدام C # و .NET Framework أو Java Spring Framework. غالبية المقالات تلبي احتياجات المجموعة السابقة.
في هذه المقالة ، سأفعل بالضبط ما ذكرته للتو أن عددًا كبيرًا جدًا من المقالات تقوم به ، ولكن في مقالة متابعة ، سنقوم بإعادة تشكيل قاعدة الرموز الخاصة بنا بالكامل ، مما يسمح لي بشرح مبادئ مثل حقن التبعية ، ثلاثة- بنية الطبقة (وحدة تحكم / خدمة / مستودع) ، تعيين البيانات والسجل النشط ، أنماط التصميم ، الوحدة ، التكامل ، واختبار الطفرات ، مبادئ SOLID ، وحدة العمل ، الترميز مقابل الواجهات ، أفضل ممارسات الأمان مثل HSTS ، CSRF ، NoSQL و SQL Injection الوقاية ، وهلم جرا. سنقوم أيضًا بالترحيل من MongoDB إلى PostgreSQL ، باستخدام أداة إنشاء الاستعلامات البسيطة Knex بدلاً من ORM - مما يسمح لنا ببناء البنية التحتية للوصول إلى البيانات الخاصة بنا والاقتراب والشخصية من لغة الاستعلام الهيكلية ، وأنواع العلاقات المختلفة (واحد- إلى واحد ، ومتعدد ، وما إلى ذلك) ، والمزيد. هذه المقالة ، إذن ، يجب أن تروق للمبتدئين ، ولكن يجب أن تلبي المقالات القليلة القادمة المزيد من المطورين المتوسطين الذين يتطلعون إلى تحسين بنيتهم المعمارية.
في هذا الكتاب ، سنقلق فقط بشأن استمرار بيانات الكتاب. لن نتعامل مع مصادقة المستخدم أو تجزئة كلمة المرور أو البنية أو أي شيء معقد من هذا القبيل. كل هذا سيأتي في المقالات التالية والمستقبلية. في الوقت الحالي ، وبشكل أساسي ، سنقوم فقط ببناء طريقة تسمح للعميل بالاتصال بخادم الويب الخاص بنا عبر بروتوكول HTTP لحفظ معلومات الكتاب في قاعدة بيانات.
ملحوظة : لقد أبقيت الأمر بسيطًا للغاية عن قصد وربما لا يكون عمليًا هنا لأن هذا المقال ، في حد ذاته ، طويل للغاية ، لأنني حرصت على الانحراف لمناقشة موضوعات تكميلية. وبالتالي ، سنعمل تدريجياً على تحسين جودة وتعقيد واجهة برمجة التطبيقات عبر هذه السلسلة ، ولكن مرة أخرى ، لأنني أعتبر هذا أحد مقدماتك الأولى لـ Express ، فأنا عن قصد أبقي الأمور بسيطة للغاية.
- تدمير كائن ES6
- اختصار كائن ES6
- عامل انتشار ES6 (...]
- قادم...
تدمير كائن 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 (التعبير عن الوظيفة الذي تم استدعاؤه فورًا) عن طريق الخطأ بوظيفة في سطر سابق (في حالة وجود إحدى هذه الوظائف) ، والأقواس حول بيان المهمة مطلوبة من أجل منع 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);
لكن وضع الصيغة المذكورة داخل توقيع الوظيفة يؤدي إلى التدمير تلقائيًا ويوفر لنا سطرًا.
توجد حالة استخدام في العالم الحقيقي لهذا في المكونات الوظيفية لـ 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. (وهناك العديد من الأسباب الأخرى ، أنا فقط أستخدم React كمثال).
import React, { useState } from "react"; export default () => { const [buttonText, setButtonText] = useState("Default"); return ( <button onClick={() => setButtonText("Toggled")}> {buttonText} </button> ); }
إشعار يتم إتلاف useState
من التصدير ، ويتم إتلاف وظائف / قيم الصفيف من ربط 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
، على النحو التالي. لاحظ أيضًا أن المصنع يعيد عنصرًا ضمنيًا ، واضحًا من خلال الأقواس حول أقواس دالة السهم.
const person = createPersonFactory('Jamie', 'Texas', 'Developer'); console.log(person); // { ... }
هذا ما نعرفه بالفعل من التركيب الحرفي للكائن ES5. لاحظ ، مع ذلك ، في وظيفة المصنع ، أن قيمة كل خاصية هي نفس اسم معرف الخاصية (المفتاح) نفسه. وهذا هو - 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 (...)
يسمح لنا عامل الانتشار بالقيام بمجموعة متنوعة من الأشياء ، والتي سنناقش بعضها هنا.
أولاً ، يمكننا نشر الخصائص من كائن إلى كائن آخر:
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'];
لاحظ هنا أننا أنشأنا اتحاد كلتا المجموعتين (المصفوفات) عن طريق نشر المصفوفات في مصفوفة جديدة.
هناك الكثير لمشغل الباقي / الانتشار ، لكنه خارج نطاق هذه المقالة. يمكن استخدامه للوصول إلى عدة حجج لوظيفة ما ، على سبيل المثال. إذا كنت ترغب في معرفة المزيد ، قم باستعراض وثائق MDN هنا.
ES6 Async / انتظار
Async / Await هو بناء جملة لتخفيف آلام سلسلة الوعد.
تسمح لك الكلمة الأساسية المحجوزة في await
"انتظار" تسوية الوعد ، ولكن لا يجوز استخدامها إلا في الوظائف المميزة بالكلمة الأساسية غير async
. افترض أن لدي وظيفة ترجع إلى الوعد. في وظيفة غير متزامنة جديدة ، يمكنني await
نتيجة هذا الوعد بدلاً من استخدام async
و .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
" عليها ستعيد وعدًا افتراضيًا.
لنفترض أنني بحاجة إلى إجراء استدعائين لواجهة برمجة التطبيقات ، أحدهما بالاستجابة من الأولى. باستخدام الوعود وتسلسل الوعد ، يمكنك القيام بذلك على النحو التالي:
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
، يمكننا إعادة بناء العوامل التالية:
const main = async () => { const resultOne = await makeAPICall('/whatever'); const resultTwo = await makeAPICall(resultOne + ' second call'); console.log(resultTwo + ' logged'); };
إليكم ما سيحدث. ستتوقف الوظيفة بأكملها عن التنفيذ في أول عبارة await
حتى يحل الوعد من الاستدعاء الأول لـ makeAPICall
، عند الحل ، سيتم وضع القيمة التي تم حلها في 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
ستعيد الوعد. لذلك ، إذا كنت تريد استدعاء دالة غير متزامنة من وظيفة أخرى ، يمكنك استخدام الوعود العادية ، أو await
إذا أعلنت أن وظيفة الاستدعاء غير async
. ومع ذلك ، إذا كنت ترغب في استدعاء دالة غير async
من كود المستوى الأعلى وتنتظر النتيجة ، .then
.catch
.
علي سبيل المثال:
const returnNumberOne = async () => 1; returnNumberOne().then(value => console.log(value)); // 1
أو يمكنك استخدام تعبير دالة تم استدعاؤه فورًا (IIFE):
(async () => { const value = await returnNumberOne(); console.log(value); // 1 })();
عند استخدام " await
" في دالة غير async
، سيتوقف تنفيذ الوظيفة عند بيان الانتظار هذا حتى يستقر الوعد. ومع ذلك ، فإن جميع الوظائف الأخرى مجانية لمتابعة التنفيذ ، وبالتالي لا يتم تخصيص موارد إضافية لوحدة المعالجة المركزية ولا يتم حظر مؤشر الترابط على الإطلاق. سأقول ذلك مرة أخرى - ستتوقف العمليات في تلك الوظيفة المحددة في ذلك الوقت المحدد حتى يتم الوفاء بالوعد ، لكن جميع الوظائف الأخرى مجانية إطلاقها. ضع في اعتبارك خادم ويب HTTP - على أساس كل طلب ، تكون جميع الوظائف مجانية لإطلاقها لجميع المستخدمين في نفس الوقت عند تقديم الطلبات ، فالأمر مجرد أن بناء الجملة غير المتزامن / المنتظر سيوفر الوهم بأن العملية متزامنة ومنعها من أجل القيام يعد أسهل في العمل معه ، ولكن مرة أخرى ، سيظل كل شيء على ما يرام وغير متزامن.
هذا ليس كل ما عليك عدم async
/ await
، ولكن من المفترض أن يساعدك على فهم المبادئ الأساسية.
مصانع OOP الكلاسيكية
نحن الآن بصدد مغادرة عالم JavaScript ودخول عالم Java . يمكن أن يأتي وقت تكون فيه عملية إنشاء كائن (في هذه الحالة ، مثيل فئة - مرة أخرى ، Java) معقدة إلى حد ما أو عندما نريد إنتاج كائنات مختلفة بناءً على سلسلة من المعلمات. قد يكون أحد الأمثلة دالة تقوم بإنشاء كائنات خطأ مختلفة. المصنع هو نمط تصميم شائع في البرمجة الشيئية وهو في الأساس وظيفة تخلق كائنات. لاستكشاف هذا ، دعنا نبتعد عن JavaScript إلى عالم Java. سيكون هذا منطقيًا للمطورين الذين يأتون من OOP الكلاسيكي (أي ليس النموذج الأولي) ، خلفية لغة مكتوبة بشكل ثابت. إذا لم تكن أحد هؤلاء المطورين ، فلا تتردد في تخطي هذا القسم. يعد هذا انحرافًا صغيرًا ، وبالتالي إذا أدى المتابعة هنا إلى مقاطعة تدفق JavaScript ، فيرجى تخطي هذا القسم مرة أخرى.
يسمح لنا نمط المصنع ، وهو نمط إبداعي شائع ، بإنشاء كائنات دون الكشف عن منطق العمل المطلوب لأداء الإنشاء المذكور.
لنفترض أننا نكتب برنامجًا يسمح لنا بتصور الأشكال البدائية بأبعاد n. إذا قدمنا مكعبًا ، على سبيل المثال ، فسنرى مكعبًا ثنائي الأبعاد (مربع) ، ومكعب ثلاثي الأبعاد (مكعب) ، ومكعب رباعي الأبعاد (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 أسهل). بالنسبة إلى كائنات العدو التي يتم إنشاء مثيل لها بطرق مختلفة ، ستسمح الواجهة بإنشاء كائن مصنع مع الاعتماد على نوع الواجهة العامة. سيكون هذا مهمًا جدًا إذا تم إنشاء العدو بشكل ديناميكي.
مثال آخر هو وظيفة البناء. لنفترض أننا نستخدم نمط التفويض للحصول على عمل مفوض للفصل لفئات أخرى تحترم الواجهة. يمكننا وضع طريقة build
ثابتة على الفئة لجعلها تنشئ مثيلها الخاص (على افتراض أنك لم تستخدم حاوية / إطار عمل لحقن التبعية). بدلاً من الاضطرار إلى استدعاء كل واضع ، يمكنك القيام بذلك:
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()); } }
سأشرح نمط التفويض في مقال لاحق إذا لم تكن على دراية به - بشكل أساسي ، من خلال التركيب ومن حيث نمذجة الكائن ، فإنه ينشئ علاقة "has-a" بدلاً من "is-a" العلاقة التي تحصل عليها مع الميراث. إذا كان لديك فئة من Mammal
وفئة Dog
، Dog
يمتد إلى Mammal
، فإن Dog
هو أحد Mammal
. بينما ، إذا كان لديك فصل Bark
، وقمت للتو بتمرير أمثلة من Bark
إلى مُنشئ Dog
، فإن Dog
has-a Bark
. كما قد تتخيل ، فإن هذا يجعل اختبار الوحدة أسهل بشكل خاص ، حيث يمكنك حقن النماذج وتأكيد الحقائق حول النموذج طالما أن الوهمي يفي بعقد الواجهة في بيئة الاختبار.
تقوم طريقة المصنع static
"build" أعلاه ببساطة بإنشاء كائن جديد User
وتمرير خدمة MessageService
ملموسة فيه. لاحظ كيف يتبع ذلك من التعريف أعلاه - عدم كشف منطق الأعمال لإنشاء كائن من فئة ، أو في هذه الحالة ، عدم تعريض إنشاء خدمة المراسلة لمتصل المصنع.
مرة أخرى ، هذه ليست بالضرورة الطريقة التي ستفعل بها الأشياء في العالم الحقيقي ، لكنها تقدم فكرة وظيفة / طريقة المصنع بشكل جيد. قد نستخدم حاوية حقن التبعية بدلاً من ذلك ، على سبيل المثال. عد الآن إلى JavaScript.
بدءا من اكسبرس
Express هو إطار عمل تطبيق ويب للعقدة (متاح عبر وحدة NPM) التي تسمح بإنشاء خادم ويب HTTP. من المهم ملاحظة أن Express ليس الإطار الوحيد للقيام بذلك (يوجد Koa و Fastify وما إلى ذلك) ، وأنه ، كما رأينا في المقالة السابقة ، يمكن أن تعمل Node بدون Express ككيان مستقل. (Express هو مجرد وحدة تم تصميمها لـ Node - يمكن للعقدة القيام بالعديد من الأشياء بدونها ، على الرغم من أن Express تحظى بشعبية لخوادم الويب).
مرة أخرى ، اسمحوا لي أن أقوم بتمييز مهم للغاية. يوجد انقسام موجود بين Node / JavaScript و Express. يمكن للعقدة ، وقت التشغيل / البيئة التي تشغل فيها JavaScript ، القيام بالعديد من الأشياء - مثل السماح لك بإنشاء تطبيقات React Native ، وتطبيقات سطح المكتب ، وأدوات سطر الأوامر ، وما إلى ذلك - Express ليست سوى إطار عمل خفيف الوزن يسمح لك باستخدام Node / JS لبناء خوادم الويب بدلاً من التعامل مع شبكة Node منخفضة المستوى وواجهات برمجة تطبيقات HTTP. لا تحتاج إلى Express لبناء خادم ويب.
قبل البدء في هذا القسم ، إذا لم تكن معتادًا على طلبات HTTP و HTTP (GET ، POST ، إلخ) ، فأنا أشجعك على قراءة القسم المقابل من مقالتي السابقة ، والمرتبط أعلاه.
باستخدام Express ، سنقوم بإعداد مسارات مختلفة يمكن تقديم طلبات HTTP إليها ، بالإضافة إلى نقاط النهاية ذات الصلة (وهي وظائف رد الاتصال) التي سيتم إطلاقها عند تقديم طلب إلى هذا المسار. لا تقلق إذا كانت المسارات ونقاط النهاية غير حساسة حاليًا - سأشرحها لاحقًا.
على عكس المقالات الأخرى ، سآخذ منهج كتابة الكود المصدري كما ننتقل ، سطرًا بسطر ، بدلاً من تفريغ قاعدة الكود بأكملها في مقتطف واحد ثم شرحها لاحقًا. لنبدأ بفتح محطة طرفية (أنا أستخدم Terminus أعلى Git Bash على Windows - وهو خيار جيد لمستخدمي Windows الذين يريدون Bash Shell دون إعداد نظام Linux الفرعي) ، وإعداد النموذج المعياري لمشروعنا ، وفتحه في Visual Studio Code.
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 app
— app
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 لنقل الأصول الثابتة إلى خادم) على المنفذ 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. في بيئة التطوير الخاصة بنا ، عند استخدام جهاز الكمبيوتر الخاص بنا كخادم التطوير الخاص بنا ، localhost
والمنفذ 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 ولكن ليس كلها.
العودة إلى التعبير
لقد ذكرت طلبات HTTP والأفعال ورموز الحالة في مقالتي السابقة ، ابدأ مع العقدة: مقدمة لواجهات برمجة التطبيقات و HTTP و ES6 + JavaScript. إذا لم يكن لديك فهم عام للبروتوكول ، فلا تتردد في الانتقال إلى قسم "طلبات HTTP و HTTP" من تلك القطعة.
من أجل التعود على Express ، سنقوم ببساطة بإعداد نقاط النهاية الخاصة بنا للعمليات الأساسية الأربع التي سنقوم بتنفيذها على قاعدة البيانات - الإنشاء والقراءة والتحديث والحذف ، والمعروفة مجتمعة باسم CRUD.
تذكر أننا نصل إلى نقاط النهاية عن طريق المسارات الموجودة في عنوان URL. وهذا يعني أنه على الرغم من استخدام الكلمتين "المسار" و "نقطة النهاية" بشكل شائع بالتبادل ، فإن نقطة النهاية هي من الناحية الفنية إحدى وظائف لغة البرمجة (مثل وظائف السهم ES6) التي تقوم ببعض العمليات من جانب الخادم ، في حين أن المسار هو ما توجد نقطة النهاية خلفه من . نحدد نقاط النهاية هذه كوظائف رد اتصال ، والتي سيتم إطلاقها Express عند تقديم الطلب المناسب من العميل إلى المسار الذي تعيش خلفه نقطة النهاية. يمكنك تذكر ما سبق من خلال إدراك أن نقاط النهاية هي التي تؤدي وظيفة وأن المسار هو الاسم المستخدم للوصول إلى نقاط النهاية. كما سنرى ، يمكن ربط نفس المسار بنقاط نهاية متعددة باستخدام أفعال HTTP مختلفة (على غرار طريقة التحميل الزائد إذا كنت تأتي من خلفية OOP كلاسيكية مع تعدد الأشكال).
ضع في اعتبارك أننا نتبع هندسة REST (نقل الحالة التمثيلية) من خلال السماح للعملاء بتقديم طلبات إلى خادمنا. هذا ، بعد كل شيء ، هو REST أو RESTful API. ستؤدي الطلبات المحددة التي يتم إجراؤها إلى مسارات محددة إلى إطلاق نقاط نهاية محددة ستقوم بأشياء محددة. مثال على مثل هذا "الشيء" الذي قد تفعله نقطة النهاية هو إضافة بيانات جديدة إلى قاعدة بيانات ، وإزالة البيانات ، وتحديث البيانات ، وما إلى ذلك.
يعرف Express ما هي نقطة النهاية التي سيتم إطلاقها لأننا نخبرها بوضوح طريقة الطلب (GET و POST وما إلى ذلك) والمسار - نحدد الوظائف التي يجب إطلاقها لمجموعات محددة من العناصر المذكورة أعلاه ، ويقوم العميل بتقديم الطلب ، مع تحديد الطريق والطريقة. لتوضيح ذلك بشكل أكثر بساطة ، مع Node ، سنخبر Express - "مرحبًا ، إذا قدم شخص ما طلب GET لهذا المسار ، فتابع وأطلق هذه الوظيفة (استخدم نقطة النهاية هذه)". يمكن أن تصبح الأمور أكثر تعقيدًا: "سريعًا ، إذا قدم شخص ما طلب GET لهذا المسار ، لكنه لم يرسل رمز حامل ترخيص صالحًا في رأس طلبه ، فيرجى الرد بـ HTTP 401 Unauthorized
. إذا كان لديهم رمز حامل صالح ، فالرجاء إرسال أي مورد محمي كانوا يبحثون عنه بإطلاق نقطة النهاية. شكرًا جزيلاً ونتمنى لك يومًا سعيدًا ". في الواقع ، سيكون من الرائع أن تكون لغات البرمجة بهذا المستوى العالي دون تسريب الغموض ، لكنها مع ذلك توضح المفاهيم الأساسية.
تذكر أن نقطة النهاية ، بطريقة ما ، تعيش خلف الطريق. لذلك من الضروري أن يقدم العميل ، في عنوان الطلب ، الطريقة التي يريد استخدامها حتى يتمكن Express من معرفة ما يجب فعله. سيتم تقديم الطلب إلى مسار محدد ، والذي سيحدده العميل (جنبًا إلى جنب مع نوع الطلب) عند الاتصال بالخادم ، مما يسمح لـ Express بالقيام بما يحتاج إلى القيام به ونحن نفعل ما نحتاج إلى القيام به عندما يقوم Express بإطلاق عمليات الاسترجاعات الخاصة بنا . هذا ما يعود إليه كل شيء.
في أمثلة الكود سابقًا ، أطلقنا على وظيفة listen
التي كانت متوفرة في app
، ونمرر إليها منفذًا واستدعاء. app
نفسه ، إذا كنت تتذكر ، هو النتيجة المرتجعة من استدعاء المتغير express
كدالة (أي ، express()
) ، والمتغير express
هو ما أطلقنا عليه نتيجة الإرجاع من طلب 'express'
من مجلد node_modules
بنا. تمامًا مثل listen
إلى app
، نحدد نقاط نهاية طلب HTTP من خلال الاتصال بها على 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 إلى localhost:3000/my-test-route
. إذا استخدمنا سلسلة مختلفة كأول وسيط أعلاه ، يجب أن يكون عنوان URL مختلفًا لمطابقة ما حددناه في JavaScript.
عندما تتحدث عن مثل هذه الأشياء ، من المحتمل أن تسمع عن أنماط Glob. يمكننا القول أن جميع مسارات API الخاصة بنا موجودة في localhost:3000/**
Glob Pattern ، حيث **
عبارة عن حرف بدل يعني أي دليل أو دليل فرعي (لاحظ أن المسارات ليست أدلة) التي يكون الجذر أصلًا لها - ذاك كل شئ.
دعنا نمضي قدمًا ونضيف بيان السجل إلى وظيفة رد الاتصال هذه بحيث يكون لدينا معًا:
// 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 أو دفق (التدفقات جميلة بشكل خاص) أو أي شيء آخر.
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
route ، فسترى HTML يتم تقديمه.
ستتيح لك علامة تبويب الشبكة في Chrome Developer Tools رؤية طلب GET هذا بمزيد من التفاصيل من حيث صلته بالرؤوس.
في هذه المرحلة ، سيكون من المفيد لنا أن نبدأ في التعرف على Express Middleware - الوظائف التي يمكن إطلاقها عالميًا بعد أن يقوم العميل بتقديم طلب.
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 Injections ، ثم تسليم req
إلى نقطة النهاية.
res
، كما رأينا سابقًا ، يسمح لك بإرسال البيانات مرة أخرى إلى العميل بعدة طرق مختلفة.
next
هي دالة رد نداء يجب عليك تنفيذها عندما تنتهي البرمجيات الوسيطة من أداء وظيفتها من أجل استدعاء وظيفة البرامج الوسيطة التالية في المكدس أو نقطة النهاية. تأكد من ملاحظة أنه سيتعين عليك استدعاء هذا في الكتلة then
أي وظائف غير متزامنة تطلقها في البرنامج الوسيط. اعتمادًا على العملية غير المتزامنة الخاصة بك ، قد ترغب أو لا ترغب في الاتصال بها في كتلة 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
. في use
الداخلي ، يتم استدعاء myMiddlware
، ويتم تمرير هذه الوسائط الثلاث. next
هو وظيفة محددة قيد use
. myMiddleware
بأنه callback
نداء في طريقة use
. إذا كنت قد use
، في هذا المثال ، كائنًا يسمى app
، فيمكننا محاكاة إعداد Express تمامًا ، وإن كان ذلك بدون أي مآخذ توصيل أو اتصال بالشبكة.
في هذه الحالة ، يعد كل من myMiddleware
و callback
دالات ذات ترتيب أعلى ، لأن كلاهما يأخذ الدوال كوسيطات.
إذا قمت بتنفيذ هذا الرمز ، فسترى الاستجابة التالية:
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
متاحًا في 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 يتم إرسالها في طلب من العميل إلى req.body
، بينما يقوم express.urlencoded
بحزم أي طلبات واردة بسلاسل أو مصفوفات أو بيانات أخرى مشفرة لعنوان URL على req.body
. باختصار ، كلاهما يعالج req.body
، لكن .json()
مخصص لـ JSON Payloads و .urlencoded()
من بين أمور أخرى ، معامِلات استعلام POST.
هناك طريقة أخرى لقول ذلك وهي أن الطلبات الواردة التي تحتوي على Content-Type: application/json
header (مثل تحديد نص POST باستخدام واجهة برمجة تطبيقات fetch
) ستتم معالجتها بواسطة express.json()
، بينما الطلبات التي تحتوي على رأس Content-Type: application/x-www-form-urlencoded
سيتم التعامل مع Content-Type: application/x-www-form-urlencoded
(مثل نماذج HTML) باستخدام express.urlencoded()
. نأمل أن يكون هذا الآن منطقيًا.
بدء مسارات CRUD الخاصة بنا لـ MongoDB
ملاحظة : عند تنفيذ طلبات التصحيح في هذه المقالة ، لن نتبع مواصفات JSONPatch RFC - وهي مشكلة سنقوم بتصحيحها في المقالة التالية من هذه السلسلة.
بالنظر إلى أننا نتفهم أننا نحدد كل نقطة نهاية من خلال استدعاء الوظيفة ذات الصلة في app
، وتمريرها إلى المسار ووظيفة رد الاتصال التي تحتوي على كائنات الطلب والاستجابة ، يمكننا البدء في تحديد مسارات CRUD الخاصة بنا لـ Bookshelf 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 ، يجب أن نناقش الطريقة الصحيحة لتسمية المسارات. مرة أخرى ، يجب عليك إلقاء نظرة على قسم HTTP في مقالتي السابقة لمزيد من المعلومات. نحن نتعامل مع الكتب ، لذلك ستكون جميع المسارات خلف /books
(اصطلاح التسمية الجمع هو المعيار).
طلب | طريق |
---|---|
بريد | /books |
احصل على | /books/id |
رقعة قماشية | /books/id |
حذف | /books/id |
كما ترى ، لا يلزم تحديد المعرف عند نشر كتاب لأننا (أو بالأحرى MongoDB) ، سنقوم بإنشائه لنا تلقائيًا من جانب الخادم. سيتطلب الحصول على الكتب وتصحيحها وحذفها جميعًا أن نقوم بتمرير هذا المعرف إلى نقطة النهاية الخاصة بنا ، والتي سنناقشها لاحقًا. الآن ، لنقم ببساطة بإنشاء نقاط النهاية:
// 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 (محدد موقع المعلومات) ، قم بزيارة localhost:3000/books
. يجب أن ترى بالفعل المؤشر في المحطة الطرفية لنظام التشغيل الخاص بك أن الخادم يعمل بالإضافة إلى بيان السجل لـ GET.
حتى الآن ، كنا نستخدم مستعرض ويب لتنفيذ طلبات GET. يعد هذا أمرًا جيدًا للبدء للتو ، ولكننا سنجد بسرعة أن هناك أدوات أفضل لاختبار مسارات API. في الواقع ، يمكننا لصق مكالمات fetch
مباشرة في وحدة التحكم أو استخدام بعض الخدمات عبر الإنترنت. في حالتنا ، ولتوفير الوقت ، سنستخدم cURL
و Postman. أستخدم كلاهما في هذه المقالة (على الرغم من أنه يمكنك استخدام أحدهما أو) حتى أتمكن من تقديمهما إذا لم تستخدمهما. cURL
هي مكتبة (مكتبة مهمة جدًا جدًا) وأداة سطر أوامر مصممة لنقل البيانات باستخدام بروتوكولات مختلفة. Postman هي أداة قائمة على واجهة المستخدم الرسومية لاختبار واجهات برمجة التطبيقات. بعد اتباع إرشادات التثبيت ذات الصلة لكلتا الأداتين على نظام التشغيل الخاص بك ، تأكد من أن الخادم الخاص بك لا يزال قيد التشغيل ، ثم قم بتنفيذ الأوامر التالية (واحدة تلو الأخرى) في محطة طرفية جديدة. من المهم أن تقوم بكتابتها وتنفيذها بشكل فردي ، ثم مشاهدة رسالة السجل في الجهاز المنفصل عن الخادم الخاص بك. لاحظ أيضًا أن رمز تعليق لغة البرمجة القياسية //
ليس رمزًا صالحًا في 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
كما ترى ، يمكن أن يكون المعرف الذي يتم تمريره كمعامل 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 - لغة الاستعلام الهيكلية). كمرجع ، قد ترغب في قراءة دليل البدء السريع Mongoose وقسم MongoDB في مقالتي السابقة.
لدينا الآن قاعدة بيانات جاهزة لقبول عمليات CRUD. Mongoose هي وحدة عقدة (أو ODM - Object Document Mapper) التي ستسمح لنا بتنفيذ هذه العمليات (تلخيص بعض التعقيدات) بالإضافة إلى إعداد مخطط أو هيكل مجموعة قاعدة البيانات.
كإخلاء مهم للمسؤولية ، هناك الكثير من الجدل حول ORMs وأنماط مثل Active Record أو Data Mapper. يقسم بعض المطورين بـ ORMs والبعض الآخر يقسمون ضدهم (معتقدين أنهم يقفون في طريقهم). من المهم أيضًا ملاحظة أن ORMs تجرد كثيرًا مثل تجميع الاتصالات ، وصلات المقابس ، والمعالجة ، إلخ. يمكنك بسهولة استخدام برنامج MongoDB Native Driver (وحدة NPM أخرى) ، لكنه سيتحدث كثيرًا عن العمل. بينما يُنصح باللعب مع Native Driver قبل استخدام ORMs ، إلا أنني حذفت Native Driver هنا للإيجاز. بالنسبة لعمليات SQL المعقدة على قاعدة بيانات علائقية ، لن يتم تحسين جميع ORMs لسرعة الاستعلام ، وقد ينتهي بك الأمر بكتابة 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:
لقب
ISBN Number
مؤلف
الاسم الأول
الكنية
Publishing Date
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.
-
title
will be of typeString
, it's a required field, and we'll trim any whitespace. -
isbn
will be of typeString
, it's a required field, it must match the validator, and we'll trim any whitespace. -
author
is of typeobject
containing a required, trimmed,string
firstName and a required, trimmed,string
lastName. -
publishingDate
is of type String (although we could make it of typeDate
orNumber
for a Unix timestamp. -
finishedReading
is a requiredboolean
that will default tofalse
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
لتحديد أننا نريد استخدام أحدث إصدار متاح من برنامج التشغيل الرسمي.
بشكل افتراضي ، إذا قمت بتعيين الفهارس (وتسمى "الفهارس" وليس "المؤشرات") (والتي لن نغطيها في هذه المقالة) على البيانات الموجودة في قاعدة البيانات الخاصة بك ، فإن ensureIndex()
المتوفرة من برنامج التشغيل الأصلي. قام MongoDB بإهمال هذه الوظيفة لصالح createIndex()
، وبالتالي فإن تعيين العلامة useCreateIndex
على true سيخبر Mongoose باستخدام طريقة createIndex()
من برنامج التشغيل ، وهي الوظيفة غير المهملة.
إصدار Mongoose الأصلي من findOneAndUpdate
(وهي طريقة للعثور على مستند في قاعدة بيانات وتحديثه) يسبق إصدار برنامج التشغيل الأصلي. وهذا يعني أن findOneAndUpdate()
لم تكن في الأصل وظيفة Native Driver بل بالأحرى وظيفة مقدمة من Mongoose ، لذلك كان على Mongoose استخدام findAndModify
التي يوفرها السائق وراء الكواليس لإنشاء وظيفة findOneAndUpdate
. مع تحديث برنامج التشغيل الآن ، فإنه يحتوي على هذه الوظيفة الخاصة به ، لذلك لا يتعين علينا استخدام findAndModify
. قد لا يكون هذا منطقيًا ، ولا بأس بذلك - إنها ليست معلومة مهمة على مقياس الأشياء.
أخيرًا ، أوقفت MongoDB نظام مراقبة الخادم والمحرك القديم. نستخدم الطريقة الجديدة مع useUnifiedTopology: true
.
ما لدينا حتى الآن هو وسيلة للاتصال بقاعدة البيانات. ولكن هذا هو الشيء - إنه غير قابل للتطوير أو فعال. عندما نكتب اختبارات الوحدة لواجهة برمجة التطبيقات هذه ، ستستخدم اختبارات الوحدة بيانات الاختبار الخاصة بها (أو التركيبات) في قواعد بيانات الاختبار الخاصة بها. لذلك ، نريد طريقة تمكننا من إنشاء اتصالات لأغراض مختلفة - بعضها لبيئات الاختبار (يمكننا تدويرها وتفكيكها حسب الرغبة) ، والبعض الآخر لبيئات التطوير ، والبعض الآخر لبيئات الإنتاج. للقيام بذلك ، سنقوم ببناء مصنع. (تذكر ذلك من قبل؟)
الاتصال بـ Mongo - بناء تنفيذ لمصنع JS
في الواقع ، لا تشبه كائنات Java على الإطلاق كائنات JavaScript ، وبالتالي ، لن يتم تطبيق ما نعرفه أعلاه من نمط تصميم المصنع. لقد قدمت ذلك فقط كمثال لإظهار النمط التقليدي. للوصول إلى كائن في Java أو C # أو C ++ ، وما إلى ذلك ، يتعين علينا إنشاء مثيل لفئة. يتم ذلك باستخدام الكلمة الأساسية new
، والتي ترشد المحول البرمجي إلى تخصيص ذاكرة للكائن في الكومة. في C ++ ، يعطينا هذا مؤشرًا للكائن الذي يتعين علينا تنظيفه لأنفسنا حتى لا يكون لدينا مؤشرات معلقة أو تسرب للذاكرة (لا يحتوي C ++ على جامع قمامة ، على عكس Node / V8 المبني على C ++) في JavaScript ، أعلاه لا يلزم القيام به - لسنا بحاجة إلى إنشاء مثيل لفئة للحصول على كائن - الكائن هو {}
فقط. سيقول بعض الناس أن كل شيء في JavaScript هو كائن ، على الرغم من أن هذا ليس صحيحًا من الناحية الفنية لأن الأنواع البدائية ليست كائنات.
للأسباب المذكورة أعلاه ، سيكون JS Factory الخاص بنا أبسط ، حيث يلتزم بالتعريف الفضفاض للمصنع باعتباره وظيفة تقوم بإرجاع كائن (كائن JS). نظرًا لأن الوظيفة هي كائن (بالنسبة function
ترث من object
عبر وراثة النموذج الأولي) ، فإن المثال أدناه سيلبي هذا المعيار. لتنفيذ المصنع ، سأقوم بإنشاء مجلد جديد داخل server
يسمى db
. في db
، سأُنشئ ملفًا جديدًا يسمى mongoose.js
. سيقوم هذا الملف بإجراء اتصالات بقاعدة البيانات. داخل mongoose.js
، connectionFactory
وظيفة تسمى connectFactory وأصدرها افتراضيًا:
// 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 التي تُرجع عبارة واحدة على نفس السطر مثل توقيع الطريقة ، سأجعل هذا الملف أبسط من خلال التخلص من تعريف 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')();
يمكنك عكس التحكم من خلال توفير عنوان URL الخاص بـ MongoDB كمعامل لوظيفة المصنع ، لكننا سنقوم بتغيير عنوان URL ديناميكيًا كمتغير بيئة بناءً على البيئة.
تتمثل فوائد إجراء اتصالنا كوظيفة في أنه يمكننا استدعاء هذه الوظيفة لاحقًا في التعليمات البرمجية للاتصال بقاعدة البيانات من الملفات التي تهدف إلى الإنتاج وتلك التي تهدف إلى اختبار التكامل المحلي والبعيد على الجهاز ومع خط أنابيب CI / CD بعيد / بناء الخادم.
بناء نقاط النهاية لدينا
نبدأ الآن في إضافة منطق بسيط للغاية يتعلق بـ CRUD إلى نقاط النهاية الخاصة بنا. كما ذكرنا سابقًا ، هناك إخلاء قصير في النظام. إن الأساليب التي نتبعها في تنفيذ منطقنا التجاري هنا ليست تلك التي يجب أن تعكسها في أي شيء آخر غير المشاريع البسيطة. يعد الاتصال بقواعد البيانات وتنفيذ المنطق مباشرة داخل نقاط النهاية أمرًا (ويجب أن يكون) مستاءً ، لأنك تفقد القدرة على تبديل الخدمات أو نظم إدارة قواعد البيانات دون الحاجة إلى إجراء إعادة بناء تطبيق على نطاق واسع. ومع ذلك ، بالنظر إلى أن هذا مقال للمبتدئين ، فإنني أستخدم هذه الممارسات السيئة هنا. ستناقش مقالة مستقبلية في هذه السلسلة كيف يمكننا زيادة كل من تعقيد وجودة هندستنا.
في الوقت الحالي ، دعنا نعود إلى ملف server.js
بنا ونتأكد من أن لدينا نفس نقطة البداية. لاحظ أنني أضفت بيان الطلب لمصنع اتصال قاعدة البيانات الخاص بنا وقمت باستيراد النموذج الذي قمنا ./models/book.js
require
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
قابل للإنشاء. لإنشاء كتاب جديد ، نقوم باستدعاء المنشئ وتمرير بيانات الكتاب ، على النحو التالي:
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
على المثيل الذي نعود إليه من استدعاء دالة المُنشئ سيستمر الكائن req.body.book
في قاعدة البيانات إذا وفقط إذا كان يتوافق مع المخطط الذي حددناه في نموذج Mongoose. إن عملية حفظ البيانات في قاعدة بيانات هي عملية غير متزامنة ، وتعيد طريقة save()
هذه وعدًا - وهو التسوية التي ننتظرها كثيرًا. بدلاً من السلسلة على استدعاء .then .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 لإعادة كائن book
مباشرة إلى العميل في حالة النجاح باستخدام res.send({ book })
- والذي سيكون مساويًا لـ res.send({ book: book })
. أعيد أيضًا التعبير فقط للتأكد من خروج وظيفتي. في كتلة catch
، قمت بتعيين الحالة لتكون 400 بشكل صريح ، وأعدت السلسلة "ValidationError" في خاصية error
للكائن الذي يتم إرساله مرة أخرى. 201 هو رمز حالة مسار النجاح الذي يعني "تم إنشاؤه".
في الواقع ، ليس هذا هو الحل الأفضل إما لأننا لا نستطيع حقًا التأكد من أن سبب الفشل كان طلبًا سيئًا من جانب العميل. ربما فقدنا الاتصال (من المفترض أن يكون اتصال مأخذ التوصيل قد تم إسقاطه ، وبالتالي استثناء عابر) بقاعدة البيانات ، وفي هذه الحالة يجب أن نعيد على الأرجح خطأ خادم داخلي 500. تتمثل إحدى طرق التحقق من ذلك في قراءة كائن الخطأ 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' }); } } });
انطلق وافتح Postman (على افتراض أنه لديك ، وإلا فقم بتنزيله وتثبيته) وقم بإنشاء طلب جديد. سنقوم بإرسال طلب POST إلى localhost:3000/books
. ضمن علامة التبويب "Body" داخل قسم طلب ساعي البريد ، سأحدد زر الاختيار "خام" وحدد "JSON" في زر القائمة المنسدلة في أقصى اليمين. سيؤدي هذا إلى إضافة Content-Type: application/json
header تلقائيًا إلى الطلب. سأقوم بعد ذلك بنسخ ولصق كتاب JSON Object من وقت سابق في منطقة النص الأساسي. هذا ما نملكه:
بعد ذلك ، سأضغط على زر الإرسال ، وسترى استجابة 201 تم إنشاؤها في قسم "الاستجابة" في Postman (الصف السفلي). نرى هذا لأننا طلبنا من Express على وجه التحديد الرد بـ 201 وكائن Book - لو أننا انتهينا للتو من res.send()
بدون رمز الحالة ، لكان البريد express
قد استجاب تلقائيًا بـ 200 OK. كما ترى ، يتم الآن حفظ كائن الكتاب في قاعدة البيانات وإعادته إلى العميل كاستجابة لطلب POST.
إذا قمت بعرض مجموعة كتاب قاعدة البيانات من خلال 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 والخوادم والمنافذ وعناوين IP وما إلى ذلك. أصبحت الأشياء مثيرة للاهتمام عندما علمنا أن هناك طرقًا متاحة على نتيجة الإرجاع من require('express')();
بأسماء أفعال HTTP ، مثل app.get
و app.post
.
إذا كان ذلك require('express')()
لم يكن منطقيًا بالنسبة لك ، فهذه هي النقطة التي كنت أثيرها:
const express = require('express'); const app = express(); app.someHTTPVerb
يجب أن يكون منطقيًا بنفس الطريقة التي أطلقنا بها مصنع التوصيل من قبل لـ Mongoose.
يتم تمرير كل معالج توجيه ، وهو وظيفة نقطة النهاية (أو وظيفة رد الاتصال) ، في كائن req
وكائن res
من Express خلف الكواليس. (هم أيضًا من الناحية الفنية next
، كما سنرى بعد دقيقة). req
يحتوي على بيانات خاصة بالطلب الوارد من العميل ، مثل الرؤوس أو أي JSON تم إرساله. res
هي ما يسمح لنا بإعادة الردود إلى العميل. يتم تمرير الوظيفة next
أيضًا إلى معالجات.
مع Mongoose ، رأينا كيف يمكننا الاتصال بقاعدة البيانات بطريقتين - طريقة بدائية وطريقة أكثر تقدمًا / عملية تستعير من نمط المصنع. سننتهي باستخدام هذا عندما نناقش اختبار الوحدة والتكامل مع Jest (واختبار الطفرات) لأنه سيسمح لنا بتدوير مثيل اختبار لقاعدة البيانات المملوءة ببيانات أولية يمكننا من خلالها تشغيل التأكيدات.
بعد ذلك ، أنشأنا كائن مخطط Mongoose واستخدمناه لإنشاء نموذج ، ثم تعلمنا كيف يمكننا استدعاء مُنشئ هذا النموذج لإنشاء مثيل جديد له. يتوفر على المثيل طريقة save
(من بين أمور أخرى) ، وهي غير متزامنة بطبيعتها ، والتي ستتحقق من أن بنية الكائن التي مررناها تتوافق مع المخطط ، وحل الوعد إذا كان كذلك ، ورفض الوعد ValidationError
من الصحة إذا لم يحدث ذلك. في حالة وجود حل ، يتم حفظ المستند الجديد في قاعدة البيانات ونستجيب بـ HTTP 200 OK / 201 CREATED ، وإلا فإننا نكتشف الخطأ الذي تم إلقاؤه في نقطة النهاية الخاصة بنا ، ونعيد HTTP 400 Bad Request إلى العميل.
بينما نواصل بناء نقاط النهاية الخاصة بنا ، ستتعلم المزيد حول بعض الطرق المتاحة في النموذج ومثيل النموذج.
الانتهاء من نقاط النهاية لدينا
بعد إكمال نقطة نهاية POST ، دعنا نتعامل مع GET. كما ذكرت سابقًا ، فإن بناء جملة :id
داخل المسار يتيح لـ Express معرفة أن id
هو معلمة مسار يمكن الوصول إليها من req.params
. لقد رأيت بالفعل أنه عند مطابقة بعض المعرفات الخاصة بـ "حرف البدل" في المسار ، تمت طباعته على الشاشة في الأمثلة المبكرة. على سبيل المثال ، إذا قدمت طلب GET إلى "/ books / test-id-123" ، فسيكون req.params.id
هو السلسلة test- id
test-id-123
لأن اسم المعلمة كان معرفًا من خلال وجود المسار كـ HTTP GET /books/:id
.
لذلك ، كل ما نحتاج إلى القيام به هو استرداد هذا المعرف من كائن req
والتحقق لمعرفة ما إذا كان أي مستند في قاعدة البيانات الخاصة بنا يحتوي على نفس المعرف - وهو أمر سهل للغاية بواسطة Mongoose (والمحرك الأصلي).
app.get('/books/:id', async (req, res) => { const book = await Book.findById(req.params.id); console.log(book); res.send({ book }); });
يمكنك أن ترى أن الوصول إلى نموذجنا هو وظيفة يمكننا تسميتها والتي ستعثر على مستند بواسطة معرفه. خلف الكواليس ، سوف يرسل Mongoose أي معرف نمرره إلى findById
إلى نوع الحقل _id
في المستند ، أو في هذه الحالة ، ObjectId
. إذا تم العثور على معرّف مطابق (وسيتم العثور على واحد فقط لمعرّف ObjectId
مع احتمال تصادم منخفض للغاية) ، فسيتم وضع هذا المستند في متغير book
الثابت. إذا لم يكن الأمر كذلك ، فسيكون book
فارغًا - وهي حقيقة سنستخدمها في المستقبل القريب.
في الوقت الحالي ، دعنا نعيد تشغيل الخادم (يجب إعادة تشغيل الخادم إلا إذا كنت تستخدم nodemon
) والتأكد من أنه لا يزال لدينا مستند الكتاب الواحد من قبل داخل مجموعة Books
. انسخ معرّف ذلك المستند ، الجزء المميز من الصورة أدناه:
واستخدمها لتقديم طلب GET إلى /books/:id
مع ساعي البريد على النحو التالي (لاحظ أن بيانات الجسم متبقية للتو من طلب POST السابق. لا يتم استخدامها فعليًا على الرغم من حقيقة أنها موضحة في الصورة أدناه) :
عند القيام بذلك ، يجب أن تحصل على مستند الكتاب بالمعرف المحدد مرة أخرى داخل قسم استجابة ساعي البريد. لاحظ أنه في وقت سابق ، مع مسار POST ، المصمم لـ "POST" أو "دفع" موارد جديدة إلى الخادم ، استجبنا بـ 201 Created - لأنه تم إنشاء مورد جديد (أو مستند). في حالة GET ، لم يتم إنشاء أي شيء جديد - لقد طلبنا للتو موردًا بمعرف محدد ، وبالتالي فإن رمز الحالة 200 OK هو ما استعدناه ، بدلاً من 201 Created.
كما هو شائع في مجال تطوير البرمجيات ، يجب حساب حالات الحافة - إدخال المستخدم غير آمن وخاطئ بطبيعته ، ومن واجبنا ، كمطورين ، أن نكون مرنين لأنواع المدخلات التي يمكن أن نقدمها والرد عليها وفقا لذلك. ماذا نفعل إذا مرر لنا المستخدم (أو متصل واجهة برمجة التطبيقات) بعض المعرفات التي لا يمكن تحويلها إلى MongoDB ObjectID ، أو معرف يمكن إرساله ولكنه غير موجود؟
بالنسبة للحالة الأولى ، سوف يقوم Mongoose بإلقاء خطأ CastError
- وهو أمر مفهوم لأننا إذا قدمنا معرفًا مثل math-is-fun
، فمن الواضح أن هذا ليس شيئًا يمكن إرساله إلى ObjectID ، والضغط على ObjectID هو بالتحديد ما النمس يفعل تحت غطاء محرك السيارة.
بالنسبة للحالة الأخيرة ، يمكننا بسهولة تصحيح المشكلة عن طريق فحص Null Check أو شرط الحرس. في كلتا الحالتين ، سأقوم بإرسال استجابة HTTP 404 Not Found. سأوضح لك بعض الطرق التي يمكننا من خلالها القيام بذلك ، بطريقة سيئة ثم طريقة أفضل.
أولاً ، يمكننا القيام بما يلي:
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()
ستلقي بـ CastError
إذا كان لا يمكن تحويل سلسلة المعرف إلى ObjectID ، مما يتسبب في تنفيذ كتلة catch
. إذا كان من الممكن إرساله ولكن معرّف الكائن المقابل غير موجود ، فسيكون book
null
وسيؤدي التحقق من القيمة الفارغة إلى حدوث خطأ ، مما يؤدي مرة أخرى إلى إطلاق كتلة catch
. catch
الداخلي ، نعيد 404. هناك مشكلتان هنا. أولاً ، حتى إذا تم العثور على الكتاب ولكن حدث خطأ آخر غير معروف ، فإننا نعيد إرسال 404 عندما يجب أن نعطي العميل 500. ثانيًا ، لا نفرق حقًا بين ما إذا كان المعرف المرسل صالحًا أم لا. غير موجود ، أو ما إذا كان مجرد هوية سيئة.
إذن ، إليك طريقة أخرى:
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 عام. لاحظ أنه بعد التحقق من عدم وجود book
، أستخدم الكلمة الأساسية return
في إجابتي. هذا مهم جدًا لأننا نريد التأكد من خروجنا من معالج المسار هناك.
قد تكون بعض الخيارات الأخرى بالنسبة لنا للتحقق مما إذا كان id
الموجود في req.params
يمكن تحويله إلى ObjectID بشكل صريح بدلاً من السماح لـ Mongoose بالإرسال ضمنيًا باستخدام mongoose.Types.ObjectId.isValid('id);
، ولكن هناك حالة حافة بسلاسل 12 بايت تؤدي إلى عمل هذا بشكل غير متوقع في بعض الأحيان.
يمكننا أن نجعل التكرار المذكور أقل إيلامًا باستخدام Boom
، أو مكتبة استجابة HTTP ، على سبيل المثال ، أو يمكننا استخدام Error Handling Middleware. يمكننا أيضًا تحويل أخطاء النمس إلى شيء أكثر قابلية للقراءة باستخدام Mongoose Hooks / Middleware كما هو موضح هنا. هناك خيار إضافي يتمثل في تحديد كائنات الخطأ المخصصة واستخدام البرامج الوسيطة السريعة للتعامل مع الخطأ السريع ، ومع ذلك ، سأحفظ ذلك لمقال قادم حيث نناقش أساليب معمارية أفضل.
في نقطة النهاية لـ PATCH /books/:id
، نتوقع أن يتم تمرير كائن تحديث يحتوي على تحديثات للكتاب المعني. بالنسبة لهذه المقالة ، سنسمح بتحديث جميع الحقول ، ولكن في المستقبل ، سأوضح كيف يمكننا عدم السماح بتحديثات حقول معينة. بالإضافة إلى ذلك ، سترى أن منطق معالجة الخطأ في نقطة نهاية التصحيح الخاصة بنا سيكون هو نفسه نقطة نهاية GET الخاصة بنا. هذا مؤشر على أننا ننتهك مبادئ 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
تأخذ معرف المستند المعني والتحديثات المراد تنفيذها وكائن خيارات اختياري. عادةً ، لن يقوم 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' }); } } });
وبذلك تكون واجهة برمجة التطبيقات الأولية الخاصة بنا كاملة ويمكنك اختبارها عن طريق تقديم طلبات HTTP لجميع نقاط النهاية.
إخلاء قصير من المسؤولية عن الهندسة المعمارية وكيف سنقوم بتصحيحه
من وجهة نظر معمارية ، الشفرة التي لدينا هنا سيئة للغاية ، إنها فوضوية ، ليست جافة ، ليست صلبة ، في الواقع ، قد تسميها بغيضة. تقوم ما يسمى بـ "معالجات الطريق" بأكثر من مجرد "تسليم المسارات" - فهي تتفاعل بشكل مباشر مع قاعدة البيانات الخاصة بنا. هذا يعني أنه لا يوجد أي تجريد على الإطلاق.
دعنا نواجه الأمر ، لن تكون معظم التطبيقات بهذه الصغر أبدًا أو ربما يمكنك الابتعاد عن البنى بدون خادم باستخدام قاعدة بيانات Firebase. ربما ، كما سنرى لاحقًا ، يريد المستخدمون القدرة على تحميل الصور الرمزية والاقتباسات والمقتطفات من كتبهم ، وما إلى ذلك. سنفتح تطبيقنا للسماح للمستخدمين باستعارة الكتب مع بعضهم البعض مقابل رسوم بسيطة - وعند هذه النقطة نحتاج إلى التفكير في تكامل الدفع مع Stripe API ولوجستيات الشحن مع Shippo API.
لنفترض أننا نواصل العمل مع بنيتنا الحالية وأضفنا كل هذه الوظائف. ستنتهي معالجات المسار هذه ، والمعروفة أيضًا باسم إجراءات التحكم ، إلى أن تكون كبيرة جدًا جدًا مع درجة عالية من التعقيد السيكلومي . قد يناسبنا أسلوب الترميز هذا جيدًا في الأيام الأولى ، ولكن ماذا لو قررنا أن بياناتنا مرجعية وبالتالي تعد PostgreSQL خيارًا أفضل لقاعدة البيانات من MongoDB؟ يتعين علينا الآن إعادة تشكيل تطبيقنا بالكامل ، وإزالة النمس ، وتغيير وحدات التحكم الخاصة بنا ، وما إلى ذلك ، وكل ذلك يمكن أن يؤدي إلى أخطاء محتملة في بقية منطق الأعمال. ومن الأمثلة الأخرى على ذلك اتخاذ قرار بأن AWS S3 باهظ التكلفة ونرغب في الانتقال إلى برنامج "شركاء Google المعتمدون". مرة أخرى ، يتطلب هذا إعادة بناء على مستوى التطبيق.
على الرغم من وجود العديد من الآراء حول الهندسة المعمارية ، بدءًا من التصميم المستند إلى المجال ، وفصل مسؤولية استعلام الأوامر ، وتحديد مصادر الأحداث ، إلى التطوير المدفوع باختبار ، و SOILD ، والهندسة المعمارية ذات الطبقات ، وهندسة البصل ، والمزيد ، سنركز على تنفيذ بنية الطبقات البسيطة في المقالات المستقبلية ، التي تتكون من وحدات التحكم والخدمات والمستودعات ، واستخدام أنماط التصميم مثل التركيب والمحولات / الأغلفة وانعكاس التحكم عبر حقن التبعية. بينما ، إلى حد ما ، يمكن تنفيذ ذلك إلى حد ما باستخدام JavaScript ، سننظر في خيارات TypeScript لتحقيق هذه البنية أيضًا ، مما يسمح لنا باستخدام نماذج البرمجة الوظيفية مثل Either 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 ، ولكن بشكل عام ، بدلاً من استخدام if/else if/else
لتحديد حالات الخطأ ، يمكنك التبديل فوق مُنشئ الخطأ. ومع ذلك سأترك ما لدينا.
في معالج نقطة نهاية / مسار متزامن ، إذا أخطأت ، فسيقوم Express بإمساكه ومعالجته دون الحاجة إلى عمل إضافي من جانبك. لسوء الحظ ، هذا ليس هو الحال بالنسبة لنا. نحن نتعامل مع كود غير متزامن . من أجل تفويض معالجة الأخطاء إلى Express باستخدام معالجات التوجيه غير المتزامن ، فإننا نلاحظ الخطأ كثيرًا ونمرره إلى 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 إلى مشاركة الموارد عبر المنشأ وهي آلية يمكن من خلالها تنفيذ الطلبات عبر المجالات. في الحالة المذكورة أعلاه ، نظرًا لأن الخادم ورمز JS للواجهة الأمامية موجودان في مجالات مختلفة ، فستقوم بتقديم طلب عبر أصلين مختلفين ، والذي يتم تقييده عادةً بواسطة المتصفح لأسباب أمنية ، ويتم تخفيفه عن طريق توفير رؤوس HTTP محددة.
سياسة المنشأ نفسه هي ما يؤدي تلك القيود المذكورة أعلاه - متصفح الويب سيسمح فقط بالمطالبات التي يتم إجراؤها عبر نفس المصدر.
سنتطرق إلى CORS و SOP لاحقًا عندما نبني واجهة Webpack مجمعة للواجهة الأمامية لـ 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
- خدمات
- Repositories
- 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.