-
المساهمات
189 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو Ola Abbas
-
يُعَد تطبيق جداول بيانات جوجل Google Sheets تطبيقًا مجانيًا ومستندًا إلى الويب ومتاحًا لأيّ شخص لديه حساب جوجل Google أو عنوان جيميل Gmail، وأصبح منافسًا مفيدًا وغنيًا بالميزات لبرنامج إكسل Excel. تحتوي جداول بيانات جوجل على واجهة برمجة تطبيقات API خاصة بها، ولكن يمكن أن تكون هذه الواجهة مربكةً بعض الشيء في عملية التعلم والاستخدام. سنغطّي في هذا المقال وحدة EZSheets الخارجية والموثقة على موقعها الرسمي، والتي لا تُعَد كاملة الميزات مثل واجهة برمجة تطبيقات جداول البيانات الرسمية من جوجل، ولكنها تسهّل تنفيذ مهام جداول البيانات الشائعة. تثبيت وإعداد وحدة EZSheets يمكنك تثبيت وحدة EZSheets من خلال فتح نافذة طرفية جديدة وتشغيل الأمر pip install --user ezsheets، وستثبِّت وحدة EZSheets أيضًا كجزء من هذا التثبيت الوحدات google-api-python-client و google-auth-httplib2 و google-auth-oauthlib، حيث تسمح هذه الوحدات لبرنامجك بتسجيل الدخول إلى خوادم جوجل وإنشاء طلبات واجهة برمجة التطبيقات API. تعالج وحدة EZSheets عملية التفاعل مع هذه الوحدات، لذلك لا داعي للقلق بشأن كيفية عملها. الحصول على الاعتماديات Credentials والملفات المفتاحية Token Files يجب تفعيل جداول بيانات جوجل وواجهات برمجة تطبيقات جوجل درايف Google Drive على حسابك على جوجل قبل أن تتمكّن من استخدام وحدة EZSheets. انتقل إلى صفحتي الويب التاليتين وانقر على زر التفعيل Enable API الموجودة في أعلى كل منهما: sheets.googleapis.com drive.googleapis.com يجب أيضًا أن تحصل على ثلاثة ملفات، والتي يجب حفظها في المجلد نفسه لسكربت بايثون Python الذي امتداده .py ويستخدم وحدة EZSheets، وهذه الملفات هي: ملف الاعتماديات واسمه credentials-sheets.json. مفتاح Token جداول بيانات جوجل واسمه token-sheets.pickle. مفتاح Token جوجل درايف واسمه token-drive.pickle. يولّد ملف الاعتماديات ملفات المفاتيح، وأسهل طريقة للحصول على ملف الاعتماديات هي الانتقال إلى صفحة Google Sheets Python Quickstart والنقر على زر التفعيل الملون باللون الأزرق Enable the Google Sheets API كما هو موضح في الشكل التالي، ولكن يجب أن تسجّل الدخول إلى حسابك في جوجل لعرض هذه الصفحة: الحصول على ملف credentials.json سيؤدي النقر على هذا الزر إلى ظهور نافذة تحتوي على رابط تنزيل ضبط العميل Download Client Configuration الذي يتيح لك تنزيل ملف credentials.json. أعِد تسمية هذا الملف إلى الاسم credentials-sheets.json وضعه في المجلد نفسه لسكربتات بايثون الخاصة بك. شغّل الأمر import ezsheets لاستيراد وحدة EZSheets بعد الحصول على الملف credentials-sheets.json، حيث ستفتح نافذة متصفح جديدة لتتمكّن من تسجيل الدخول إلى حسابك على جوجل عند استيراد وحدة EZSheets في المرة الأولى. انقر بعد ذلك على زر السماح Allow كما هو موضح في الشكل التالي: السماح لصفحة Python Quickstart بالوصول إلى حسابك على جوجل سبب ظهور الرسالة السابقة هو أنك نزّلتَ ملف الاعتماديات من صفحة Google Sheets Python Quickstart، وستفتح هذه النافذة مرتين: الأولى للوصول إلى جداول بيانات جوجل والثانية للوصول إلى جوجل درايف، حيث تستخدم وحدةُ EZSheets الوصولَ إلى جوجل درايف لرفع جداول البيانات وتنزيلها وحذفها. ستطالبك نافذة المتصفح بإغلاقه بعد تسجيل الدخول، وسيظهر الملفان token-sheets.pickle و token-drive.pickle في المجلد نفسه الذي يوجد فيه الملف credentials-sheets.json. ستجري هذه العملية فقط في المرة الأولى التي تشغِّل فيها الأمر import ezsheets. إذا واجهتَ خطأً بعد النقر على زر السماح "Allow" وكانت الصفحة معطَّلة، فتأكّد أولًا من تفعيل جداول بيانات جوجل وواجهات برمجة تطبيقات جوجل درايف من الروابط الموجودة في بداية هذا القسم. قد يستغرق الأمر بضع دقائق حتى تتمكّن خوادم جوجل من تسجيل هذا التغيير، لذا قد تضطر إلى الانتظار قبل أن تتمكّن من استخدام وحدة EZSheets. ملاحظة: لا تشارك ملفات الاعتماديات أو المفاتيح مع أيّ شخص، وتعامل معها مثل كلمات المرور. إبطال ملف الاعتماديات إذا شاركتَ ملفات الاعتماديات أو المفاتيح مع شخصٍ ما عن طريق الخطأ، فلن يتمكّن هذا الشخص من تغيير كلمة مرور حسابك على جوجل، ولكن سيكون لديه حق الوصول إلى جداول بياناتك. يمكنك إبطال هذه الملفات بالانتقال إلى صفحة طرفية المطور على منصة سحابة جوجل Google Cloud Platform، ولكن يجب تسجيل الدخول إلى حسابك على جوجل لعرض هذه الصفحة. انقر على رابط الاعتماديات Credentials في الشريط الجانبي، ثم انقر على أيقونة سلة المهملات بجانب ملف الاعتماديات الذي شاركته عن طريق الخطأ، كما هو موضح في الشكل التالي: صفحة الاعتماديات في طرفية المطور على منصة سحابة جوجل يمكن إنشاء ملف اعتماديات جديد من هذه الصفحة من خلال النقر على زر إنشاء الاعتماديات "Create Credentials" وتحديد خيار معرّف عميل OAuth أو "OAuth Client ID" كما هو موضح في الشكل السابق، ثم حدّد الخيار "أخرى Other" بالنسبة لنوع التطبيق وسمِّ الملف بأيّ اسم تريده. سيُدرَج بعد ذلك ملف الاعتماديات الجديد في الصفحة، ويمكنك النقر على أيقونة التنزيل لتنزيله. سيكون للملف الذي ستنزِّله اسم ملف طويل ومعقد، لذا يجب إعادة تسميته إلى اسم الملف الافتراضي الذي تحاول الوحدة EZSheets تحميله وهو credentials-sheets.json. يمكنك أيضًا إنشاء ملف اعتماديات جديد من خلال النقر على زر تفعيل واجهة برمجة تطبيقات جداول بيانات جوجل "Enable the Google Sheets API" المذكور في القسم السابق. كائنات جدول البيانات Spreadsheet يمكن أن يحتوي جدول البيانات Spreadsheet في جداول بيانات جوجل على أوراق Sheets متعددة والتي تُسمَّى أيضًا أوراق عمل Worksheets، وتحتوي كل ورقة على أعمدة Columns وصفوف Rows من القيم. يوضح الشكل التالي جدول بيانات بعنوان بيانات التعليم "Education Data"، والذي يحتوي على ثلاث أوراق بعنوان الطلاب "Students" والصفوف "Classes" والموارد "Resources"، ويُسمَّى العمود الأول من كل ورقة A، ويسمى الصف الأول 1: جدول بيانات بعنوان "Education Data" مكوَّن من ثلاث أوراق سيتمثّل معظم عملك في تعديل كائنات الورقة Sheet، ولكن يمكنك أيضًا تعديل كائنات جدول البيانات Spreadsheet، كما سنوضّح في القسم التالي. إنشاء جداول البيانات وتحميلها وسردها يمكنك إنشاء كائن Spreadsheet جديد من جدول بيانات موجود مسبقًا أو جدول بيانات فارغ أو جدول بيانات مرفوع على جداول بيانات جوجل، حيث يمكن إنشاء كائن Spreadsheet من جدول بياناتٍ موجود مسبقًا على جداول بيانات جوجل، ولكن أن تعرف السلسلة النصية لمعرّف جدول البيانات. يمكن العثور على المعرّف الفريد لجداول بيانات جوجل في عنوان URL، بعد الجزء spreadsheets/d/ وقبل الجزء /edit، فمثلًا يوجد جدول البيانات الموضّح في الشكل السابق على عنوان URL الذي هو https://docs.google.com/spreadsheets/d/1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU/edit#gid=151537240/ وبالتالي يكون معرّفه 1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU. ملاحظة: معرّفات جداول البيانات المُستخدَمة في هذا المقال خاصة بجداول بيانات حساب جوجل الخاص بالكاتب، إذ لن تعمل إذا أدخلتها في صدفتك التفاعلية Interactive Shell، لذا انتقل إلى جداول بيانات جوجل لإنشاء جداول بيانات ضمن حسابك ثم احصل على المعرّفات من شريط العناوين. مرّر معرّف جدول بياناتك بوصفه سلسلةً نصية إلى الدالة ezsheets.Spreadsheet() للحصول على كائن Spreadsheet لجدول البيانات الخاص بهذا المعرّف: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss Spreadsheet(spreadsheetId='1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss.title 'Education Data' يمكنك أيضًا الحصول على كائن Spreadsheet لجدول بيانات موجود مسبقًا من خلال تمرير عنوان URL الكامل لجدول البيانات إلى تلك الدالة، أو إذا كان هناك جدول بيانات واحد فقط في حسابك على جوجل له العنوان نفسه، فيمكنك تمرير عنوان جدول البيانات بوصفه سلسلة نصية. يمكنك إنشاء جدول بيانات جديد وفارغ من خلال استدعاء الدالة ezsheets.createSpreadsheet() وتمرير سلسلةٍ نصية إليها، حيث تمثل هذه السلسلة النصية عنوان جدول البيانات الجديد. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.createSpreadsheet('Title of My New Spreadsheet') >>> ss.title 'Title of My New Spreadsheet' يمكنك رفع جدول بيانات إكسل Excel أو أوبن أوفيس OpenOffice أو CSV أو TSV موجود مسبقًا إلى جداول بيانات جوجل من خلال تمرير اسم ملف جدول البيانات إلى الدالة ezsheets.upload(). إذًا لندخِل ما يلي في الصدفة التفاعلية مع وضع اسم ملف جدول بياناتك مكان الملف my_spreadsheet.xlsx: >>> import ezsheets >>> ss = ezsheets.upload('my_spreadsheet.xlsx') >>> ss.title 'my_spreadsheet' يمكنك سرد جداول البيانات الموجودة على حسابك على جوجل من خلال استدعاء الدالة listSpreadsheets() التي تعيد قاموسًا Dictionary مفاتيحه هي معرّفات جداول البيانات وقيمه هي عناوين جداول البيانات. إذًا لندخِل ما يلي في الصدفة التفاعلية بعد رفع جدول البيانات: >>> ezsheets.listSpreadsheets() {'1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU': 'Education Data'} يمكنك بعد الحصول على كائن Spreadsheet استخدام سماته وتوابعه للتعامل مع جدول البيانات المُستضاف على جداول بيانات جوجل عبر الإنترنت. سمات Attributes كائن جدول البيانات Spreadsheet توجد البيانات الفعلية في الأوراق الخاصة بجدول البيانات، ولكن يحتوي كائن Spreadsheet على السمات title و spreadsheetId و url و sheetTitles و sheets للتعامل مع جدول البيانات. لندخِل ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss.title # عنوان جدول البيانات 'Education Data' >>> ss.title = 'Class Data' # تغيير العنوان >>> ss.spreadsheetId # المعرّف الفريد (وهو سمة للقراءة فقط) '1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU' >>> ss.url # عنوان URL الأصلي (وهو سمة للقراءة فقط) 'https://docs.google.com/spreadsheets/d/1J-Jx6Ne2K_vqI9J2SO- TAXOFbxx_9tUjwnkPC22LjeU/' >>> ss.sheetTitles # عناوين جميع كائنات الورقة Sheet ('Students', 'Classes', 'Resources') >>> ss.sheets # كائنات الورقة Sheet في جدول البيانات بالترتيب (<Sheet sheetId=0, title='Students', rowCount=1000, columnCount=26>, <Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26>, <Sheet sheetId=151537240, title='Resources', rowCount=1000, columnCount=26>) >>> ss[0] # كائن الورقة الأول في جدول البيانات <Sheet sheetId=0, title='Students', rowCount=1000, columnCount=26> >>> ss['Students'] # يمكن أيضًا الوصول إلى الأوراق باستخدام العنوان <Sheet sheetId=0, title='Students', rowCount=1000, columnCount=26> >>> del ss[0] # حذف كائن الورقة الأول في جدول البيانات >>> ss.sheetTitles # أصبح كائن الورقة "Students" محذوفًا ('Classes', 'Resources') إذا عدّل شخصٌ ما جدول البيانات من موقع جداول بيانات جوجل، فيمكن للسكربت الخاص بك تحديث كائن Spreadsheet ليطابق البيانات الموجودة على الإنترنت من خلال استدعاء التابع refresh(): >>> ss.refresh() لن يحدّث هذا التابع سمات كائن Spreadsheet فحسب، بل سيحدّث البيانات الموجودة في كائنات Sheet التي يحتوي عليها كائن Spreadsheet، وستنعكس التغييرات التي تجريها على كائن Spreadsheet في جدول البيانات الموجود على الإنترنت ضمن الزمن الحقيقي. تنزيل ورفع جداول البيانات يمكنك تنزيل جدول بيانات جوجل بعددٍ من التنسيقات مثل: إكسل وأوبن أوفيس OpenOffice و CSV و TSV و PDF، ويمكنك أيضًا تنزيله كملف مضغوط ZIP يحتوي على ملفات HTML لبيانات جدول البيانات، حيث تحتوي الوحدة EZSheets على دوالٍ لكل خيار من هذه الخيارات كما سنوضح فيما يلي: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss.title 'Class Data' >>> ss.downloadAsExcel() # تنزيل جدول البيانات كملف إكسل 'Class_Data.xlsx' >>> ss.downloadAsODS() # تنزيل جدول البيانات كملف أوبن أوفيس 'Class_Data.ods' >>> ss.downloadAsCSV() # تنزيل الورقة الأولى فقط كملف CSV 'Class_Data.csv' >>> ss.downloadAsTSV() # تنزيل الورقة الأولى فقط كملف TSV 'Class_Data.tsv' >>> ss.downloadAsPDF() # تنزيل جدول البيانات كملف PDF 'Class_Data.pdf' >>> ss.downloadAsHTML() # تنزيل جدول البيانات كملف مضغوط ZIP مؤلَّفٍ من ملفات HTML 'Class_Data.zip' لاحظ أن الملفات التي لها تنسيق CSV و TSV يمكن أن تحتوي على ورقة واحدة فقط، لذلك إذا نزّلت جدول بيانات من جداول بيانات جوجل بهذا التنسيق، فستحصل على الورقة الأولى فقط، ولكن يمكنك تنزيل أوراق أخرى من خلال تغيير السمة index الخاصة بكائن Sheet إلى القيمة 0. تعيد جميع دوال التنزيل سلسلة نصية لاسم الملف الذي جرى تنزيله، ويمكنك أيضًا تحديد اسم ملفك لجدول البيانات من خلال تمرير اسم الملف الجديد إلى دالة التنزيل كما يلي، ويجب أن تعيد الدالة اسم الملف المُحدَّث: >>> ss.downloadAsExcel('a_different_filename.xlsx') 'a_different_filename.xlsx' حذف جداول البيانات يمكننا حذف جدول بيانات من خلال استدعاء التابع delete(): >>> import ezsheets >>> ss = ezsheets.createSpreadsheet('Delete me') # إنشاء جدول البيانات >>> ezsheets.listSpreadsheets() # التأكد من إنشاء جدول بيانات {'1aCw2NNJSZblDbhygVv77kPsL3djmgV5zJZllSOZ_mRk': 'Delete me'} >>> ss.delete() # حذف جدول البيانات >>> ezsheets.listSpreadsheets() {} سينقل التابع delete() جدول بياناتك إلى مجلد سلة المهملات على جوجل درايف، حيث يمكنك عرض محتويات مجلد سلة المهملات، ولكن يمكن حذف جدول البيانات نهائيًا من خلال تمرير القيمة True لوسيط الكلمة المفتاحية Keyword Argument الذي هو permanent كما يلي: >>> ss.delete(permanent=True) لا يُعَد حذف جداول البيانات حذفًا نهائيًا فكرةً جيدة، فمن المستحيل استرداد جدول البيانات الذي أدّى خطأٌ في سكربتك إلى حذفه عن غير قصد. ليس هناك داعٍ للقلق بشأن تحرير المساحة، إذ تتوفر مساحة تخزينية بالجيجابايتات حتى في حسابات جوجل درايف المجانية. كائنات الورقة Sheet يحتوي كائن Spreadsheet على كائن Sheet واحد أو أكثر، حيث تمثّل كائنات Sheet صفوف وأعمدة البيانات الموجودة في الورقة، ويمكنك الوصول إلى هذه الأوراق باستخدام عامل الأقواس المربعة وعدد صحيح يمثل الفهرس. تحتوي السمة sheets على مجموعة Tuple من كائنات Sheet بالترتيب الذي تظهر به في جدول البيانات. يمكنك الوصول إلى كائنات Sheet في جدول البيانات من خلال إدخال ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1J-Jx6Ne2K_vqI9J2SO-TAXOFbxx_9tUjwnkPC22LjeU') >>> ss.sheets # كائنات الورقة Sheet في جدول البيانات بالترتيب (<Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26>, <Sheet sheetId=151537240, title='Resources', rowCount=1000, columnCount=26>) >>> ss.sheets[0] # الحصول على كائن الورقة الأول في جدول البيانات <Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26> >>> ss[0] # الحصول أيضًا على كائن الورقة الأول في جدول البيانات <Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26> يمكنك أيضًا الحصول على كائن Sheet باستخدام عامل الأقواس المربعة وسلسلة نصية تمثّل اسم الورقة، وتحتوي السمة sheetTitles الخاصة بكائن Spreadsheet على مجموعةٍ تمثّل جميع عناوين الأوراق. إذًا لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> ss.sheetTitles # عناوين جميع كائنات الورقة Sheet في جدول البيانات ('Classes', 'Resources') >>> ss['Classes'] # يمكن أيضًا الوصول إلى الأوراق باستخدام العنوان <Sheet sheetId=1669384683, title='Classes', rowCount=1000, columnCount=26> يمكنك بعد الحصول على كائن Sheet قراءة البيانات منه وكتابة البيانات فيه باستخدام توابع كائن Sheet كما سنوضّح في القسم التالي. قراءة وكتابة البيانات تحتوي أوراق عمل جداول بيانات جوجل على أعمدة وصفوف من الخلايا التي تحتوي على بيانات كما هو الحال في جداول بيانات إكسل، حيث يمكنك استخدام عامل الأقواس المربعة لقراءة البيانات من هذه الخلايا وكتابتها فيها. أنشئ مثلًا جدول بيانات جديد وأضِف البيانات إليه من خلال إدخال ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.createSpreadsheet('My Spreadsheet') >>> sheet = ss[0] # الحصول على الورقة الأولى في جدول البيانات >>> sheet.title 'Sheet1' >>> sheet = ss[0] >>> sheet['A1'] = 'Name' # ضبط القيمة في الخلية A1 >>> sheet['B1'] = 'Age' >>> sheet['C1'] = 'Favorite Movie' >>> sheet['A1'] # قراءة القيمة في الخلية A1 'Name' >>> sheet['A2'] # تعيد الخلايا الفارغة سلسلة نصية فارغة '' >>> sheet[2, 1] # العمود 2 والصف 1 هو عنوان الخلية B1 نفسه 'Age' >>> sheet['A2'] = 'Alice' >>> sheet['B2'] = 30 >>> sheet['C2'] = 'RoboCop' يجب أن ينتج عن هذه التعليمات جدول بيانات جوجل يشبه الشكل التالي: جدول البيانات الذي أنشأناه باستخدام تعليمات المثال السابق يمكن لعدة مستخدمين تحديث الورقة في الوقت ذاته، لذا يمكنك تحديث البيانات المحلية في كائن Sheet من خلال استدعاء التابع refresh() الخاص بهذا الكائن: >>> sheet.refresh() تُحمَّل كافة البيانات الموجودة في كائن Sheet عند تحميل كائن Spreadsheet لأول مرة، وبالتالي يمكن قراءة البيانات مباشرةً، ولكن تتطلب كتابة القيم في جدول البيانات عبر الإنترنت اتصالًا بالشبكة ويمكن أن تستغرق حوالي ثانية واحدة، حيث إذا كان لديك آلاف الخلايا التي تريد تحديثها، فقد يكون تحديثها واحدةً تلو الأخرى بطيئًا جدًا. عنونة الأعمدة والصفوف تعمل عنونة الخلايا في جداول بيانات جوجل كما هو الحال في إكسل، ولكن الفرق الوحيد بينهما هو احتواء جداول بيانات جوجل على أعمدة وصفوف تستند إلى القيمة 1، أي أن العمود أو الصف الأول موجود في الفهرس 1 وليس في الفهرس 0 على عكس فهارس القائمة المستندة إلى القيمة 0 في لغة بايثون. يمكنك تحويل العنوان الذي تنسيقه سلسلة نصية 'A2' إلى عنوانٍ تنسيقه مجموعة (column, row) (والعكس صحيح) باستخدام الدالة convertAddress(). تحوّل الدالتان getColumnLetterOf() و getColumnNumberOf() أيضًا عنوان العمود من الحروف إلى الأعداد وبالعكس. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ezsheets.convertAddress('A2') # تحويل العناوين... (1, 2) >>> ezsheets.convertAddress(1, 2) # … وتحويلها بالعكس مرة أخرى 'A2' >>> ezsheets.getColumnLetterOf(2) 'B' >>> ezsheets.getColumnNumberOf('B') 2 >>> ezsheets.getColumnLetterOf(999) 'ALK' >>> ezsheets.getColumnNumberOf('ZZZ') 18278 تُعَد العناوين التي لها تنسيق السلسلة النصية 'A2' ملائمةً لكتابة العناوين في شيفرتك المصدرية، وتكون العناوين التي لها تنسيق المجموعة (column, row) ملائمةً إذا أردتَ التكرار على مجالٍ من العناوين واحتجتَ صيغةً رقمية للعمود، لذا تُعَد الدوال convertAddress() و getColumnLetterOf() و getColumnNumberOf() مفيدةً عندما تريد التحويل بين هذين التنسيقين. قراءة وكتابة الأعمدة والصفوف بأكملها قد تستغرق كتابة البيانات ضمن خلية واحدة في كل مرة وقتًا طويلًا كما ذكرنا سابقًا، ولكن تحتوي وحدة EZSheets على توابع خاصة بكائن Sheet لقراءة وكتابة الأعمدة والصفوف بأكملها في الوقت ذاته، حيث يقرأ التابعان getColumn() و getRow() من الأعمدة والصفوف ويكتب التابعان updateColumn() و updateRow() في الأعمدة والصفوف. تنشِئ هذه التوابع طلبات إلى خوادم جداول بيانات جوجل لتحديث جدول البيانات، لذا يجب أن تكون متصلًا بالإنترنت. سنرفع في مثالنا جدول بيانات أسعار المنتجات produceSales.xlsx من المقال السابق إلى جداول بيانات جوجل، حيث تبدو الصفوف الثمانية الأولى كما في الشكل التالي: يمكنك رفع جدول البيانات produceSales.xlsx من خلال إدخال ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.upload('produceSales.xlsx') >>> sheet = ss[0] >>> sheet.getRow(1) # الصف الأول هو الصف 1 وليس الصف 0 ['PRODUCE', 'COST PER KiloGram', 'KiloGrams SOLD', 'TOTAL', '', ''] >>> sheet.getRow(2) ['Potatoes', '0.86', '21.6', '18.58', '', ''] >>> columnOne = sheet.getColumn(1) >>> sheet.getColumn(1) ['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic', --snip-- >>> sheet.getColumn('A') # النتيجة نفسها للتعليمة getColumn(1) ['PRODUCE', 'Potatoes', 'Okra', 'Fava beans', 'Watermelon', 'Garlic', --snip-- >>> sheet.getRow(3) ['Okra', '2.26', '38.6', '87.24', '', ''] >>> sheet.updateRow(3, ['Pumpkin', '11.50', '20', '230']) >>> sheet.getRow(3) ['Pumpkin', '11.50', '20', '230', '', ''] >>> columnOne = sheet.getColumn(1) >>> for i, value in enumerate(columnOne): ... # اجعل قائمة بايثون تحتوي على سلاسل نصية بأحرف كبيرة: ... columnOne[i] = value.upper() ... >>> sheet.updateColumn(1, columnOne) # تحديث العمود بأكمله في طلب واحد تسترد الدالتان getRow() و getColumn() البيانات من جميع الخلايا الموجودة في صف أو عمود محدد بوصفها قائمةً من القيم، وتصبح الخلايا الفارغة قيمًا لسلاسل نصية فارغة في القائمة. يمكنك تمرير رقم أو حرف العمود إلى الدالة getColumn() لإخبارها باسترداد بيانات عمودٍ معين، حيث وضّحنا في المثال السابق أن التعلمتين getColumn(1) و getColumn('A') تعيدان القائمة نفسها. تكتب الدالتان updateRow() و updateColumn() فوق البيانات الموجودة في الصف أو العمود على التوالي باستخدام قائمة القيم المُمرّرة إليهما، فمثلًا احتوى الصف الثالث في المثال السابق على معلومات حول البامية Okra في البداية، لكن أدّى استدعاء الدالة updateRow() إلى وضع بيانات حول اليقطين Pumpkin مكانها، ثم استدعينا الدالة sheet.getRow(3) مرةً أخرى لعرض القيم الجديدة في الصف الثالث. لنحدّث بعد ذلك جدول بيانات "produceSales"، حيث يُعَد تحديث خلية واحدة في كل مرة أمرًا بطيئًا إذا كان لديك العديد من الخلايا التي تريد تحديثها، بينما يُعَد الحصول على عمود أو صف كقائمة وتحديث القائمة ثم تحديث العمود أو الصف بأكمله باستخدام القائمة أسرع بكثير، حيث يمكن إجراء جميع التغييرات في طلبٍ واحد. يمكن الحصول على كافة الصفوف دفعةً واحدة من خلال استدعاء التابع getRows() لإعادة قائمةٍ بجميع القوائم، حيث تمثل كل قائمة من القوائم الداخلية الموجودة ضمن القائمة الخارجية صفًا واحدًا من الورقة. يمكنك تعديل هذه القيم الموجودة في هيكل البيانات لتغيير اسم المنتج Produce Name وعدد الكيلوجرامات المباعة Kilograms Sold والتكلفة الإجمالية Total لبعض الصفوف، ثم تمرّرها إلى التابع updateRows() من خلال إدخال ما يلي في الصدفة التفاعلية: >>> rows = sheet.getRows() # الحصول على جميع الصفوف في جدول البيانات >>> rows[0] # فحص القيم الموجودة في الصف الأول ['PRODUCE', 'COST PER KiloGrams', 'KiloGrams SOLD', 'TOTAL', '', ''] >>> rows[1] ['POTATOES', '0.86', '21.6', '18.58', '', ''] >>> rows[1][0] = 'PUMPKIN' # تغيير اسم المنتج >>> rows[1] ['PUMPKIN', '0.86', '21.6', '18.58', '', ''] >>> rows[10] ['OKRA', '2.26', '40', '90.4', '', ''] >>> rows[10][2] = '400' # تغيير عدد الكيلوجرامات المباعة >>> rows[10][3] = '904' # تغيير التكلفة الإجمالية >>> rows[10] ['OKRA', '2.26', '400', '904', '', ''] >>> sheet.updateRows(rows) # تحديث جدول البيانات عبر الإنترنت بالتغييرات التي أجريناها يمكنك تحديث الورقة بأكملها في طلب واحد من خلال تمرير قائمةٍ من القوائم المُعادة من الدالة getRows() والمُعدَّلة بالتغييرات التي أجريناها على الصفين 1 و 10 إلى الدالة updateRows(). لاحظ أن الصفوف الموجودة في ورقة جداول بيانات جوجل تحتوي على سلاسل نصية فارغة في نهايتها، لأن الورقة التي رفعناها تحتوي على 6 أعمدة، ولدينا 4 أعمدة فقط من البيانات. يمكنك قراءة عدد الصفوف والأعمدة في الورقة باستخدام السمتين rowCount و columnCount، ثم يمكنك تغيير حجم الورقة من خلال ضبط هاتين القيمتين. >>> sheet.rowCount # عدد الصفوف في الورقة 23758 >>> sheet.columnCount # عدد الأعمدة في الورقة 6 >>> sheet.columnCount = 4 # تغيير عدد الأعمدة إلى 4 >>> sheet.columnCount # يصبح الآن عدد الأعمدة في الورقة 4 4 يجب أن تحذف التعليمات السابقة العمودين الخامس والسادس من جدول بيانات "produceSales" كما هو موضّح في الشكل التالي: الورقة قبل (على اليسار) وبعد (على اليمين) تغيير عدد الأعمدة إلى 4. يمكن أن تحتوي جداول بيانات جوجل على ما يصل إلى 10 ملايين خلية وفقًا لمركز المساعدة في جوجل درايف، ولكن يُفضَّل أن تجعل الأوراق بالحجم الذي تحتاجه فقط لتقليل الوقت الذي يستغرقه تعديل البيانات وتحديثها. إنشاء وحذف الأوراق تبدأ جميع جداول بيانات جوجل بورقة واحدة اسمها "Sheet1"، ولكن يمكنك إضافة أوراق إضافية إلى نهاية قائمة الأوراق باستخدام التابع createSheet() الذي تمرّر إليه سلسلة نصية لاستخدامها كعنوان للورقة الجديدة، ويمكن للوسيط الثاني الاختياري الخاص بهذا التابع تحديد فهرس العدد الصحيح للورقة الجديدة. يمكنك إنشاء جدول بيانات ثم إضافة أوراق جديدة إليه من خلال إدخال ما يلي في الصدفة التفاعلية: >>> import ezsheets >>> ss = ezsheets.createSpreadsheet('Multiple Sheets') >>> ss.sheetTitles ('Sheet1',) >>> ss.createSheet('Spam') # إنشاء ورقة جديدة في نهاية قائمة الأوراق <Sheet sheetId=2032744541, title='Spam', rowCount=1000, columnCount=26> >>> ss.createSheet('Eggs') # إنشاء ورقة جديدة أخرى <Sheet sheetId=417452987, title='Eggs', rowCount=1000, columnCount=26> >>> ss.sheetTitles ('Sheet1', 'Spam', 'Eggs') >>> ss.createSheet('Meat', 0) # إنشاء ورقة عند الفهرس 0 في قائمة الأوراق <Sheet sheetId=814694991, title='Meat', rowCount=1000, columnCount=26> >>> ss.sheetTitles ('Meat', 'Sheet1', 'Spam', 'Eggs') تضيف التعليمات السابقة ثلاث أوراق جديدة إلى جدول البيانات هي: "Meat" و"Spam" و"Eggs" بالإضافة إلى الورقة الافتراضية "Sheet1". تُرتَّب الأوراق الموجودة في جدول البيانات، وتضاف الأوراق الجديدة إلى نهاية القائمة إن لم تمرِّر وسيطًا ثانيًا إلى الدالة createSheet()، حيث يحدّد هذا الوسيط فهرس الورقة. أنشأنا في المثال السابق الورقة التي عنوانها "Meat" في الفهرس 0، مما يجعل الورقة "Meat" هي الورقة الأولى في جدول البيانات وإزاحة الأوراق الثلاث الأخرى بمقدار موضعٍ واحد، ويشبه ذلك سلوك تابع القائمة insert(). يمكنك رؤية الأوراق الجديدة على التبويبات الموجودة أسفل الشاشة كما هو موضَّح في الشكل التالي: جدول بيانات الأوراق المتعددة "Multiple Sheets" بعد إضافة أوراق "Spam" و"Eggs" و"Meat" يحذف التابع delete() الخاص بالكائن Sheet ورقةً من جدول البيانات، ولكن إذا أدرتَ الاحتفاظ بالورقة مع حذف البيانات الموجودة فيها، فاستدعِ التابع clear() لمسح جميع الخلايا وجعل هذه الورقة ورقةً فارغة. إذا لندخِل ما يلي في الصدفة التفاعلية: >>> ss.sheetTitles ('Meat', 'Sheet1', 'Spam', 'Eggs') >>> ss[0].delete() # حذف الورقة الموجودة في الفهرس 0 أي الورقة "Meat" >>> ss.sheetTitles ('Sheet1', 'Spam', 'Eggs') >>> ss['Spam'].delete() # حذف الورقة "Spam" >>> ss.sheetTitles ('Sheet1', 'Eggs') >>> sheet = ss['Eggs'] # إسناد الورقة "Eggs" إلى متغير >>> sheet.delete() # حذف الورقة "Eggs" >>> ss.sheetTitles ('Sheet1',) >>> ss[0].clear() # مسح جميع الخلايا الموجودة في الورقة "Sheet1" >>> ss.sheetTitles # الورقة "Sheet1" فارغة ولكنها لا تزال موجودة ('Sheet1',) يكون حذف الأوراق حذفًا نهائيًا، إذ لا توجد طريقة لاستعادة البيانات، ولكن يمكنك إنشاء نسخة احتياطية من الأوراق من خلال نسخها إلى جدول بيانات آخر باستخدام التابع copyTo() كما سنوضّح في القسم التالي. نسخ الأوراق يحتوي كل كائن Spreadsheet على قائمةٍ مرتبة من كائنات Sheet الموجودة ضمنه، حيث يمكنك استخدام هذه القائمة لإعادة ترتيب الأوراق (كما وضّحنا في القسم السابق) أو نسخها إلى جداول بيانات أخرى، إذ يمكن نسخ كائن Sheet إلى كائن Spreadsheet آخر من خلال استدعاء التابع copyTo() الذي نمرّر إليه كائن Spreadsheet الهدف كوسيط. لندخِل ما يلي في الصدفة التفاعلية لإنشاء جدولي بيانات ونسخ بيانات جدول البيانات الأول إلى الورقة الأخرى: >>> import ezsheets >>> ss1 = ezsheets.createSpreadsheet('First Spreadsheet') >>> ss2 = ezsheets.createSpreadsheet('Second Spreadsheet') >>> ss1[0] <Sheet sheetId=0, title='Sheet1', rowCount=1000, columnCount=26> >>> ss1[0].updateRow(1, ['Some', 'data', 'in', 'the', 'first', 'row']) >>> ss1[0].copyTo(ss2) # نسخ الورقة Sheet1 الخاصة بجدول البيانات ss1 إلى جدول البيانات ss2 >>> ss2.sheetTitles # سيحتوي جدول البيانات ss2 على نسخة من الورقة Sheet1 الخاصة بجدول البيانات ss1 Sheet1 ('Sheet1', 'Copy of Sheet1') لاحظ تسمية الورقة المنسوخة بالاسم Copy of Sheet1، لأن جدول البيانات الهدف (ss2 في المثال السابق) يحتوي مسبقًا على ورقة بالاسم Sheet1. تظهر الأوراق المنسوخة في نهاية قائمة أوراق جدول البيانات الهدف، ولكن يمكنك تغيير السمة index لإعادة ترتيبها في جدول البيانات الجديد. التعامل مع الحصص Quotas في جداول بيانات جوجل تُعَد جداول بيانات جوجل متاحةً عبر الإنترنت، لذا من السهل مشاركة الأوراق بين عدة مستخدمين يمكنهم جميعًا الوصول إلى الأوراق في وقتٍ واحد، ولكن سيؤدّي ذلك إلى أن تكون قراءة الأوراق وتحديثها أبطأ من قراءة وتحديث ملفات إكسل المخزَّنة محليًا على قرص حاسوبك الصلب. تفرض جداول بيانات جوجل أيضًا قيودًا على عدد عمليات القراءة والكتابة التي يمكنك إجراؤها. يُقيَّد مستخدمو جداول بيانات جوجل بإنشاء 250 جدول بيانات جديد يوميًا، ويمكن لحسابات جوجل المجانية إجراء 100 طلب قراءة و100 طلب كتابة في كل 100 ثانية وفقًا لإرشادات مطوري جوجل، إذ ستؤدي محاولة تجاوز هذه الحصة إلى رفع الاستثناء googleapiclient.errors.HttpError أو "Quota exceeded for quota group" الذي يمثّل تجاوز الحصة المتاحة، حيث تلتقط الوحدة EZSheets تلقائيًا هذا الاستثناء وتعيد محاولة الطلب. إذا حدث ذلك، فستستغرق استدعاءات الدوال لقراءة البيانات أو كتابتها عدة ثوانٍ أو حتى دقيقة أو دقيقتين قبل أن تعيد شيئًا ما، وإذا استمر الطلب في الفشل، وهو أمرٌ ممكن إذا أجرى سكربتٌ آخر يمتلك الاعتماديات نفسها طلباتٍ أيضًا، فستعيد الوحدة EZSheets رفعَ هذا الاستثناء. يؤدي ذلك إلى أنه قد تستغرق استدعاءات توابع الوحدة EZSheets عدة ثوانٍ قبل أن تعيد شيئًا ما. إذا أردتَ عرضَ حجم استخدامك لواجهة برمجة التطبيقات أو زيادةَ حصتك، فانتقل إلى صفحة IAM & Admin Quotas للتعرف على كيفية الدفع مقابل زيادة حجم الاستخدام. إذا أردتَ التعامل مع استثناءات HttpError بنفسك، فيمكنك ضبط ezsheets.IGNORE_QUOTA على القيمة True، وسترفع توابع الوحدة EZSheets هذه الاستثناءات عندما تواجهها. مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر. برنامج لتنزيل بيانات نماذج جوجل Google Forms تتيح لك نماذج جوجل إنشاء نماذج بسيطة عبر الإنترنت تسهّل جمع المعلومات من الأشخاص، حيث تُخزَّن المعلومات التي يدخلها هؤلاء الأشخاص في النموذج ضمن جداول بيانات جوجل. جرّب كتابة برنامجٍ يمكنه تنزيل معلومات النموذج التي أرسلها المستخدمون تلقائيًا، لذا انتقل إلى نماذج جوجل وأنشئ نموذجًا جديدًا، حيث سيكون هذا النموذج فارغًا، ثم أضِف الحقول إلى النموذج الذي يطلب من المستخدم اسمه وعنوان بريده الإلكتروني، ثم انقر على زر "إرسال Send" في الجزء العلوي الأيمن للحصول على رابط لنموذجك الجديد مثل الرابط https://goo.gl/forms/QZsq5sC2Qe4fYO592/، وحاول إدخال بعض الأمثلة على الردود في هذا النموذج. انقر على الزر الأخضر "Create Spreadsheet" في تبويب "الردود Responses" في نموذجك لإنشاء جدول بيانات جوجل الذي سيحتوي على الردود التي يرسلها المستخدمون. يُفترَض أن تشاهد إجاباتك في الصفوف الأولى من جدول البيانات. اكتب بعد ذلك سكربت بايثون مع استخدام الوحدة EZSheets لجمع قائمة بعناوين البريد الإلكتروني في جدول البيانات. برنامج لتحويل جداول البيانات إلى تنسيقات أخرى يمكنك استخدام جداول بيانات جوجل لتحويل ملف جدول بيانات إلى تنسيقات أخرى، لذا جرّب كتابة سكربت يمرر ملفًا مُرسَلًا إلى الدالة upload().نزّل جدول البيانات بعد رفعه على جداول بيانات جوجل باستخدام الدوال downloadAsExcel() و downloadAsODS() وغيرها من الدوال المماثلة لإنشاء نسخة من جدول البيانات بتنسيقات أخرى. برنامج للعثور على الأخطاء في جدول البيانات لنفترض أن لدينا جدول بيانات يحتوي على إجمالي عدد حبات الفاصولياء مرفوع على جداول بيانات جوجل، حيث يكون جدول البيانات قابلًا للعرض، ولكنه غير قابل للتحرير، ويمكنك الاطلاع عليه في متصفحك والحصول عليه باستخدام الشيفرة التالية: >>> import ezsheets >>> ss = ezsheets.Spreadsheet('1jDZEdvSIh4TmZxccyy0ZXrH-ELlrwq8_YYiZrEOB4jg') أعمدة الورقة الأولى في جدول البيانات هي عدد حبات الفاصولياء في الجرة "Beans per Jar" وعدد الجِرار "Jars" وعدد حبات الفاصولياء الكلي "Total Beans"، حيث ينتج العمود "Total Beans" من ضرب الأعداد الموجودة في العمودين "Beans per Jar" و "Jars"، ولكن يوجد خطأ في أحد الصفوف البالغ عددها 15000 صفًا في هذه الورقة. يُعَد ذلك عددًا كبيرًا جدًا من الصفوف التي لا يمكن التحقق منها يدويًا، ولكن يمكنك كتابة سكربت يتحقق من العمود "Total Beans". يمكنك الوصول إلى الخلايا الفردية في صف باستخدام ss[0].getRow(rowNum)، حيث ss هو كائن Spreadsheet و rowNum هو رقم الصف، وتذكّر أن أرقام الصفوف في جداول بيانات جوجل تبدأ من العدد 1 وليس من 0. ستكون قيم الخلايا سلاسلًا نصية، لذا يجب تحويلها إلى أعداد صحيحة حتى يتمكّن برنامجك من العمل معها. يُقيَّم التعبير int(ss[0].getRow(2)[0]) * int(ss[0].getRow(2)[1]) == int(ss[0].getRow(2)[2]) على القيمة True إذا احتوى الصف على القيمة الإجمالية الصحيحة، لذا ضع هذا الشيفرة البرمجية في حلقة لتحديد الصف الموجود في الورقة الذي يحتوي على القيمة الإجمالية غير الصحيحة. الخلاصة يُعَد تطبيق جداول بيانات جوجل تطبيقًا شائعًا لجداول البيانات عبر الإنترنت التي تعمل في متصفحك. يمكنك تنزيل جداول البيانات وإنشاؤها وقراءتها وتعديلها باستخدام الوحدة الخارجية EZSheets التي تمثّل جداول البيانات بوصفها كائنات Spreadsheet التي تحتوي على قائمة مرتبة من كائنات Sheet، وتحتوي كل ورقة على أعمدة وصفوف من البيانات التي يمكنك قراءتها وتحديثها بطرق متعددة. تسهّل جداول بيانات جوجل مشاركة البيانات وتعديلها بصورة جماعية، ولكن عيبها الرئيسي هو السرعة، إذ يجب عليك تحديث جداول البيانات باستخدام طلبات الويب، مما يؤدي إلى أن يستغرق التنفيذ بضع ثوانٍ، ولكن لن يؤثر هذا القيد على سكربتات بايثون التي تستخدم وحدة EZSheets بالنسبة لمعظم الأغراض. تحدّ جداول بيانات جوجل أيضًا من عدد المرات التي يمكنك فيها إجراء التغييرات. ملاحظة: يمكنك الحصول على التوثيق الكامل لميزات الوحدة EZSheet من موقعها الرسمي. ترجمة -وبتصرُّف- للمقال Working with Google Sheets لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: الكتابة في مستندات إكسل باستخدام لغة بايثون Python قراءة مستندات جداول إكسل باستخدام لغة بايثون Python مقدمة إلى تطبيق مستندات جوجل Google Docs
-
سنتعّرف في هذا المقال على المواضيع التالية: الأسماء المختلفة التي يُعرَف بها الجداء النقطي. تعريف الجداء النقطي للأشعة الهندسية. الخاصية التبديلية للجداء النقطي للأشعة. جيب تمام Cosine الزاوية. الجداء النقطي للأشعة المتعامدة. الجداء النقطي وطول الشعاع. خاصيات الجداء النقطي للأشعة. الجداء النقطي للمصفوفات العمودية ثنائية الأبعاد. الخاصية التبديلية للجداء النقطي للمصفوفات العمودية. الجداء النقطي للمصفوفات العمودية ثلاثية الأبعاد. الجداء النقطي للمصفوفة العمودية مع نفسها. خاصية توزيع الجداء النقطي للمصفوفات العمودية. تعرّفنا حتى الآن على العمليات التالية: | جمع شعاعين | شعاع + شعاع | = | شعاع | | طرح شعاعين | شعاع - شعاع | = | شعاع | | الفرق بين نقطتين | نقطة - نقطة | = | شعاع | | تغيير حجم شعاع (ضرب شعاع بعدد حقيقي) | عدد حقيقي * شعاع | = | شعاع | سنناقش في هذا المقال الجداء النقطي Dot Product الذي يأخذ شعاعين بوصفهما معاملَين وينتج عنه عدد حقيقي بوصفه الخرج. يسمّى الجداء النقطي في بعض الأحيان بالجداء الداخلي Inner Product، ويسمّى أحيانًا أخرى بالجداء السلمي Scalar Product، والذي لا ينبغي الخلط بينه وبين العملية التي تسمى تغيير حجم الشعاع Scaling (ضرب الشعاع بعدد حقيقي). | الجداء النقطي | شعاع . شعاع | = | عدد حقيقي Scalar (مقدار سلمي) | ملاحظة: يُسمى الجداء النقطي أحيانًا بالجداء السلمي، لأنه يأخذ شعاعين وينتج عنه عدد حقيقي Scalar (مقدار سلمي). الزاوية بين شعاعين الزاوية بين الشعاعين u و v هي الزاوية θ التي تكون: 0 <= θ <= 180 درجة ويكون ذلك صحيحًا لكل من الفضاء ثنائي الأبعاد وثلاثي الأبعاد، فهذه الزاوية هي أصغر زاوية يمكن أن يدورها أحد الأشعة حتى يحاذي الشعاع الآخر. ولكن إذا كان هناك شعاعان يؤشّران إلى اتجاهين متعاكسين، فستكون الزاوية بينهما تساوي 180 درجة (أو π راديان). الجداء النقطي يكون الجداء النقطي للشعاعين u و v بالنسبة للأشعة الهندسية في الفضاء ثنائي أو ثلاثي الأبعاد هو: u · v = | u | | v | cos θ يُرمَز للجداء النقطي بنقطة بين الشعاعين، ولكن لا تظهِر العديد من المتصفحات النقطة بصورة واضحة لسوء الحظ، إذ قد ترى نقطة صغيرة جدًا أو شريطًا أسود صغيرًا، ويمثّل الرمز θ الزاوية بين هذين الشعاعين. يقع الشعاعان في مستوٍ واحد في الفضاء ثنائي الأبعاد، وبالتالي من السهل تصوّر الزاوية بينهما، بينما يقع الشعاعان في الفضاء ثلاثي الأبعاد في مستوٍ مُضمَّن في هذا الفضاء إلا في الحالة التي يؤشّر فيها الشعاعان إلى الاتجاه نفسه. تكون الزاوية بين الشعاعين صفرًا عندما يؤشّران إلى الاتجاه نفسه وبالتالي: u · v = | u | | v | cos 0 = | u | | v | 1 = | u | | v | بما أن -1 <= cos θ <= +1 فستكون قيمة نتيجة الجداء النقطي بين القيمتين -| u | | v | و +| u | | v | اعتمادًا على الزاوية بين الشعاعين، بينما تكون الزاوية بين الشعاعان 180 درجة وتكون قيمة نتيجة الجداء النقطي هي -| u | | v | عندما يؤشّر هذان الشعاعان إلى اتجاهين متعاكسين. الخاصية التبديلية Commutative للجداء النقطي إذا كان u · v = | u | | v | cos θ، فإن v · u: v · u = | v | | u | cos θ = | u | | v | cos θ = u · v تُعَد عملية الجداء النقطي عملية تبديلية، إذ لا يحدِث ترتيب المعاملات أيّ فرق: u · v = v · u cنوعان مختلفان من الأصفار في المعادلة التالية، إذ تكون معامَلات عملية الجداء النقطي شعاعَين، ويكون خرج العملية قيمة سلمية (عددًا حقيقيًا): 0 · 0 = 0 يمكن إيجاد a (u · v) حيث "a" عدد حقيقي كما يلي: a (u · v) = a | u | | v | cos θ نعيد ترتيب الصيغة السابقة، فينتج: = | a u | | v | cos θ = (a u) · v = u · (a v) جيب تمام Cosine الزاوية 90 درجة قد تكون كيفية عمل دالة جيب التمام Cosine غامضةً إلى حدٍ، لذا تخيّل دائرة الوحدة مع إسقاط نصف قطرها على المحور x كما في الشكل السابق الذي ينتج عنه ما يلي: جيب تمام الزاوية 0 درجة = 1.0 جيب تمام الزاوية 30 درجة = 0.866 جيب تمام الزاوية 45 درجة = 0.707 جيب تمام الزاوية 60 درجة = 0.500 جيب تمام الزاوية 90 درجة = 0.0 وتذكّر أن: u · v = | u | | v | cos θ الجداء النقطي للأشعة المتعامدة Orthogonal لنفترض أن لدينا شعاعين بينهما زاوية مقدارها 90 درجة، أي أنهما شعاعان متعامدان. هنا يمكن إيجاد جدائهما النقطي كما يلي: u · v = | u | | v | cos 90° = | u | | v | 0.0 = 0.0 وبالتالي نستنتج أن الجداء النقطي للأشعة المتعامدة هو صفر، ويمكن تطبيق هذه الحقيقة على الأشعة في جميع الأبعاد. نعني بالشعاعين المتعامدين أن بينهما زاوية مقدارها 90 درجة، ويُعَد الشعاع الصفري متعامدًا مع جميع الأشعة الأخرى، حيث أن 0 · v = 0.0 لجميع الأشعة v . كشف التعامد وحساب الطول لنفترض أن الشعاعين s و t ناتج الجداء النقطي لهما يساوي صفر، إذًا: ماذا يمكنك أن تقول عن المنحى النسبي Relative Orientation للشعاعين s و t؟ الشعاعان s و t متعامدان. ماذا يمكنك أن تقول عن طولي الشعاعين s و t؟ لا يمكنك قول أيّ شيء ممّا تعرفه عن الجداء النقطي. إذا كان لدينا شعاعان متعامدان فيما بينهما، فسيكون جداؤها النقطي صفرًا بغض النظر عن أطوالهما، إذ يكتشف الجداء النقطي التعامد بغض النظر عن أطوال الأشعة. ملاحظة: تعتمد إشارة الجداء النقطي لشعاعين u و v على ما إذا كانت الزاوية بينهما حادة أم منفرجة، فإذا كانت قيمة الجداء النقطي صفرًا، فستكون الأشعة متعامدة. لنطّلع الآن على الجداء النقطي للشعاع مع نفسه: v · v = | v | | v | cos 0 = | v | | v | 1.0 = | v |2 وبالتالي فإن الجداء النقطي للشعاع مع نفسه يساوي مربع طوله. | v | = √(v · v) يحسب الجداءُ النقطي الطولَ باستخدام الصيغة السابقة، وبالتالي يُعَد الجداء النقطي مفيدًا بما أن خاصيتي الشعاع هما الطول والمنحى Orientation، ولكن يمكن أن تتساءل عن إمكانية أن يكون قيمة | v | = √(v · v) سالبة أو صفرًا. حسنًا، لا يمكن أن تكون سالبة، لأن الطول يكون دائمًا صفرًا أو قيمة موجبة، ويكون المقدار | v | = √(v · v) صفرًا عندما يكون الشعاع v هو الشعاع الصفري. خاصيات الجداء النقطي سنسرد فيما يلي قائمة بخاصيات الجداء النقطي وهي: u · v = | u | | v | cos θ: حاصل الجداء النقطي لشعاعين u · v = v · u: الخاصية التبديلية للجداء النقطي لشعاعين u · v = 0 : حيث u و v شعاعين متعامدين 0 · 0 = 0 | v |2 = v · v a (u·v) = (a u) · v u . (v + w ) = (u . v) + (u . w) (v + w ) . u = (v . u) + (w . u) = (u . v) + (u . w) (au + bv) · w = (au) · w + (bv) · w: حيث a و b قيم سلمية (أعداد حقيقية) 0 . u = 0: حيث 0 (بالخط العريض) هو الشعاع الصفري، و 0 هو عدد حقيقي (مقدار سلمي) ما هو ناتج جداء u · v · w؟ ليس للجداء u · v · w معنىً، فإذا كان u · v = a (حيث a عدد حقيقي)، فإن a · w لا معنى له، وإذا كان v · w = b (حيث b عدد حقيقي)، فإن u · b لا معنى له، وليس للتعبير الأصلي أيّ معنًى في كلتا الحالتين. إذًا ما هو ناتج 0 · u؟ إذا كان "0" (بخط عادي غير عريض) عددًا حقيقيًا، فإن هذه العملية لا معنى لها. إذا كان "0" (بخط عريض) شعاعًا صفريًا، فالنتيجة هي 0 (عدد حقيقي أو مقدار سلمي). الجداء النقطي للمصفوفات العمودية يمكن استخدام الجداء النقطي أيضًا مع المصفوفات العمودية. ليكن لدينا مثلًا المصفوفتان العموديتان التاليتان: a = ( a1, a2 )T b = ( b1, b2 )T فيمكننا تعريف الجداء النقطي لهما على النحو التالي: a · b = a1b1 + a2b2 حيث نضرب العناصر المتقابلة لكل مصفوفة عمودية، ثم نجمع النواتج، وستكون نتيجة الجداء النقطي قيمة عددية (سلمية). ملاحظة: نكتب الجداء النقطي للمصفوفات العمودية في بعض الأحيان على النحو التالي: aT b (مع التعريف نفسه)، وسنوضّح لاحقًا هذه الصيغة الغريبة عند مناقشة ضرب المصفوفات. أوجد ناتج الجداء النقطي للمصفوفتين العموديتين التاليتين: a = ( 1, 2 )T b = ( 3, 4 )T والناتج هو: a · b = 1*3 + 2*4 = 3 + 8 = 11 ولنوجد الآن حاصل جداء b · a كما يلي: b · a = 3*1 + 4*2 = 1*3 + 2*4 = a · b خاصيات الجداء النقطي للمصفوفات العمودية يمتلك الجداء النقطي للمصفوفات العمودية الخاصيات نفسها للجداء النقطي للأشعة، والتي سنوضحها في الفقرات التالية، ولكن لنأخذ أولًا مثالًا لنتعرّف من خلاله على الجداء النقطي للمصفوفات العمودية. لنفترض أن لدينا المصفوفتين العموديتين التاليتين: p = ( -2, 5 )T q = ( 3, -1 )T وسيكون الجداء النقطي لهما هو: p · q = (-2)*3 + 5*( -1) = -6 + -5 = -11 وإليك مثال آخر أكثر واقعية: لنفترض أن لدينا المصفوفتين العموديتين التاليتين: s = ( 1.082, -3.224 )T t = ( 2.381, 7.009 )T وسيكون الجداء النقطي لهما هو: s · t = 1.082*2.381 + -3.224*7.009 = 2.576242 + -22.597016 = -20.020774 لاحظ أن الجداء النقطي يأخذ معاملين هما مصفوفتان عموديتان وينتج عنه قيمة عددية (سلّمية) واحدة في جميع الحالات. الخاصية التبديلية للجداء النقطي للمصفوفات العمودية لنوجد مثلًا الجداء النقطي للمصفوفتين ( -1, 3)T و ( 2, 4 )T، ثم الجداء النقطي للمصفوفتين ( 2, 4 )T و ( -1, 3)T كما يلي: ( -1, 3)T · ( 2, 4 )T = -12 + 3*4 = -2 + 12 = 10 ( 2, 4 )T · ( -1, 3)T = 2*(-1) + 4*3 = -2 + 12 = 10 وبالتالي نستنتج أن الجداء النقطي للمصفوفات العمودية هو عملية تبديلية: a · b = b · a لا يشكّل ترتيب المعاملات أيّ فرق كما هو الحال مع الجداء النقطي للأشعة، حيث يمكننا كتابة تعريف الجداء النقطي للمصفوفتين a و b في كلا الترتيبين كما يلي: a · b = ( a1, a2 )T · ( b1, b2 )T = a1b1 + a2b2 b · a = ( b1, b2 )T · ( a1, a2 )T = b1a1 + b2a2 = a1b1 + a2b2 لاحظ أن الأشياء الموجودة بعد إِشارة المساواة "=" الأخيرة هي نفسها في الحالتين. الجداء النقطي لأنواع المعاملات المختلطة لنفترض أننا نريد إيجاد الجداء النقطي للمصفوفات العمودية a و b و c: a · b · c لكن هذه العملية لا معنى له، إذ ينتج عن الجداء النقطي لشعاعين (أو مصفوفتين عموديتين) عدد حقيقي، ولا يوجد شيء اسمه الجداء النقطي لعدد حقيقي وشعاع (أو مصفوفة عمودية)، فلا معنى للعملية التالية: a · b · c = عدد حقيقي · c أو للعملية التالية: a · b · c = a · عدد حقيقي لذا يجب عليك أن تتابع أنواع المعاملات والنتائج عند التعامل مع الجداء النقطي. لكن العملية التالية لها معنًى (لاحظ أنه لا توجد نقطة بين العدد 2 والشعاع الذي يليه، وهذا يمثّل عملية تغيير الحجم أو ضرب الشعاع بعدد حقيقي Scaling، وليس الجداء النقطي): 2( -1, 2)T · ( 4, 1 )T = ( -2, 4)T · ( 4, 1 )T = -2*4 + 4*1 = -8 + 4 = -4 الجداء النقطي في فضاء ثلاثي الأبعاد نعرّف الجداء النقطي لمصفوفات عمودية ثلاثية الأبعاد كما يلي، حيث نضرب العناصر المتقابلة في المصفوفتين ثم نجمع النواتج. ليكن لدينا المصفوفتان العموديتان التاليتان: a = ( a1, a2, a3 )T b = ( b1, b2, b3 )T فيمكننا تعريف الجداء النقطي لهما على النحو التالي: a · b = a1b1 + a2b2 + a3b3 لكن يجب أن تحتوي المصفوفتان العموديتان على عدد العناصر نفسه كما في المثالين التاليين: (1, 2, 3)T · (6, 7, 😎T = 1*6 + 2*7 + 3*8 = 44 ( -1, 2, -3)T · (1, -2, 3)T = (-1)(1) + (2)(-2) + (-3)(3) = -1 + -4 + -9 = -14 ولا مانع من وجود بعض المتغيرات كعناصر للأشعة كما يلي: (1, 2, 3)T · (x, y, z)T = x + 2y + 3z الجداء النقطي مع شعاع صفري جرّب إيجاد ناتج الجداء النقطي للمصفوفتين ( 4, 0, -3)T و (0, -2, 0)T كما يلي: ( 4, 0, -3)T · (0, -2, 0)T = 4*0 + 0*(-2) + (-3)*0 = 0+0+0 = 0 لاحظ أن قيمة طول كل شعاع في الجداء النقطي السابق أكبر من الصفر، ولكن قيمة حاصل الجداء النقطي صفر. إليك مثال آخر: v(0, 0, 0)T · (-2.3, 89.22, 0)T = 0(-2.3) + 0(89.22) + 0(0) = 0 ولا يُعَد ذلك مفاجأة، إذ رأينا الخاصية التالية نفسها مع الأشعة الهندسية: 0 · a = 0 لاحظ أن 0 الأول هو المصفوفة العمودية الصفرية، والصفر الأخير هو العدد الحقيقي صفر، ويمكن أيضًا كتابة ما يلي: 0 · 0 = 0 تعني المصفوفة العمودية الصفرية في كل من هذه المعادلات مصفوفة عمودية لها بُعد المصفوفة العمودية الأخرى نفسه، وكل عنصر فيها هو العدد الحقيقي صفر. ناتج الجداء النقطي هو جميع القيم الحقيقية الممكنة أوجد ناتج الجداء النقطي للمصفوفتين (-2, 5, -6)T و ( 1, 2, 3)T: v(-2, 5, -6)T · ( 1, 2, 3)T = (-2)*1 + 5*2 + (-6)*3 = -2 + 10 -18 = -10 لاحظ أن الجواب هو عدد حقيقي سالب، إذ ينتج عن الجداء النقطي جميع أنواع القيم الحقيقية: سالبة وصفر وموجبة، ولكن هناك شيء مهم يحدث عند إجراء عملية الجداء النقطي للشعاع مع نفسه، وهذا ما سنوضّحه في الفقرة التالية. ناتج الجداء النقطي للمصفوفة العمودية مع نفسها هو قيمة موجبة أو صفرية لنوجد مثلًا الجداء النقطي للشعاع (1, -3)T مع نفسه كما يلي: v(1, -3)T · (1, -3)T = (1)(1) + (-3)(-3) = 1 + 9 = 10 يكون ناتج الجداء النقطي للمصفوفة العمودية مع نفسها موجبًا دائمًا كما هو الحال مع الأشعة. (x, y, z)T · (x, y, z)T = x2 + y2 + z2 = قيمة موجبة أو صفرية سيكون مربع x و y و z صفرًا أو أكبر من الصفر بغض النظر عن قيمها، وبالتالي سيكون المجموع صفرًا أو أكبر من الصفر، ولكن المرة الوحيدة التي يكون فيها مربع العدد الحقيقي صفرًا هي عندما يكون هذا العدد الحقيقي صفرًا، لذا فالمرة الوحيدة التي يكون فيها حاصل الجداء النقطي لمصفوفة عمودية مع نفسها صفرًا هي عندما تكون المصفوفة العمودية مصفوفة صفرية. ليكن g شعاعًا ثلاثيّ الأبعاد، فأوجد قيم عناصر هذا الشعاع من خلال حل المعادلة التالية: g · g = 0 نلاحظ أنه يجب أن يكون كل عنصر من عناصر الشعاع g صفرًا من خلال المناقشة السابقة، لذا: g = 0 = (0, 0, 0)T قابلية توزيع الجداء النقطي على الجمع يمكن توزيع الجداء النقطي على عملية الجمع كما يلي: a · (b + c) = a · b + a · c يبدو هذا جيدًا، ولكن كن حذرًا في تحديد ما تعنيه إشارة "+" على جانبي إشارة المساواة "=". تمثّل إشارة "+" في الجانب الأيسر من المعادلة عملية جمع الأشعة. تمثّل إشارة "+" في الجانب الأيمن من المعادلة عملية جمع الأعداد الحقيقية. من السهل إلى حدٍ ما إثبات أن خاصية التوزيع صحيحة، إذًا لنوضّح هذه الخاصية باستخدام المصفوفات العمودية ثلاثية الأبعاد، بالرغم من أنه يمكن استخدامها مع أيّ بُعد. ويمثّل كلا طرفي المعادلة عددًا حقيقيًا، لذا يجب التحقق من أن هذين الطرفين يمثلان العدد الحقيقي نفسه. إذًا لنبرهن أن a · (b + c) = a · b + a · c. ليكن لدينا المصفوفات العمودية التالية: a = (f, g, h)T b = (r, s, t)T c = (x, y, z)T نبدأ بالطرف الأيسر: a · (b + c) = a · ( r+x, s+y, t+z )T = f(r+x) + g(s+y) + h(t+z) = fr + fx + gs + gy + ht + hz ثم بالطرف الأيمن: a · b + a · c = (fr + gs + ht) + (fx + gy + hz) = fr + fx + gs + gy + ht + hz ينتج لدينا أن طرفي المعادلة يمثلان العدد الحقيقي نفسه، ويمكن إجراء الشيء نفسه مع الأشعة بجميع الأبعاد، وبالتالي فإن خاصية التوزيع تكون صحيحة مع جميع الأبعاد. تدريب عملي أوجد ناتج الجداء النقطي التالي: v( -2, 1, 2)T · ( (3, -1, 4)T + ( -2, 1, -2)T ) قد يكون من الأسهل عدم إعادة ترتيب التعبير، بل يُفضَّل إجراء عملية جمع الأشعة أولًا كما يلي: ( -2, 1, 2)T · ( (3, -1, 4)T + ( -2, 1, -2)T ) = ( -2, 1, 2)T · (1, 0, 2)T = -2*1 + 1*0 + 2*2 = -2 + 4 = 2 وتكون إعادة ترتيب التعبير أسهل في أحيان أخرى كما في المثال التالي، حيث يكون من الأسهل إجراء عملية الجداء النقطي أولًا: ( -2, 0, 2)T · ( (1, -1, 1)T + ( 3, 1, 4)T ) = ( -2, 0, 2)T · (1, -1, 1)T + ( -2, 0, 2)T · ( 3, 1, 4)T = 0 + 2 = 2 خاتمة وضّحنا في هذا المقال مفهوم الجداء النقطي للأشعة وخاصياته، وسنناقش في المقال التالي كيفية ارتباط الجداء النقطي بطول الشعاع. ترجمة -وبتصرُّف- للفصل The Dot Product من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: تغيير حجم شعاع Scaling في التصاميم ثلاثية الأبعاد 3D خاصية الاتجاه Direction للأشعة الهندسية في التصاميم ثلاثية الأبعاد خاصية الطول Length للأشعة الهندسية في التصاميم ثلاثية الأبعاد
-
تعرّفنا في المقال السابق على كيفية قراءة مستندات إكسل باستخدام لغة بايثون، وسنتعرّف في هذا المقال على كيفية كتابة مستندات إكسل باستخدام لغة بايثون، إذ توفّر وحدة OpenPyXL الخاصة بلغة بايثون طرقًا لكتابة البيانات، مما يعني أن برامجك يمكنها إنشاء ملفات جداول البيانات وتعديلها، ومن السهل إنشاء جداول بيانات تحتوي على آلاف الصفوف من البيانات باستخدام بايثون. إنشاء وحفظ مستندات إكسل استدعِ الدالة openpyxl.Workbook() لإنشاء كائن مصنف Workbook جديد وفارغ، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() # إنشاء مصنف فارغ >>> wb.sheetnames # يبدأ المصنف بورقة واحدة ['Sheet'] >>> sheet = wb.active >>> sheet.title 'Sheet' >>> sheet.title = 'Meat Eggs Sheet' # تغيير العنوان >>> wb.sheetnames ['Meat Eggs Sheet'] سيبدأ المصنف بورقة واحدة تُسمَّى "Sheet"، ويمكنك تغيير اسم الورقة من خلال تخزين سلسلة نصية جديدة في السمة Attribute الخاصة بها وهي title. لن يُحفظ ملف جدول البيانات عند تعديل كائن Workbook أو أوراقه وخلاياه حتى استدعاء تابع المصنف save(). أدخِل ما يلي في الصدفة التفاعلية (مع الملف example.xlsx في مجلد العمل الحالي): >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb.active >>> sheet.title = 'Spam Spam Spam' >>> wb.save('example_copy.xlsx') # حفظ المصنف غيّرنا اسم الورقة، وحفظنا التغييرات من خلال تمرير اسم الملف كسلسلة نصية إلى التابع save(). يؤدي تمرير اسم ملف مختلف عن الاسم الأصلي -مثل الاسم 'example_copy.xlsx'- إلى حفظ التغييرات في نسخة من جدول البيانات. إذا عدّلتَ جدول بيانات حمّلته من ملف، فيجب عليك دائمًا حفظ جدول البيانات الجديد المُعدَّل باسم ملفٍ مختلف عن اسم الملف الأصلي، وبذلك سيظل لديك ملف جدول البيانات الأصلي للعمل عليه في حالة وجود خطأ في شيفرتك البرمجية، مما يؤدي إلى احتواء الملف الجديد المحفوظ على بيانات غير صحيحة أو تالفة. إنشاء وحذف الأوراق يمكن إضافة الأوراق وحذفها من المصنف باستخدام التابع create_sheet() والعامل del. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> wb.sheetnames ['Sheet'] >>> wb.create_sheet() # إضافة ورقة جديدة <Worksheet "Sheet1"> >>> wb.sheetnames ['Sheet', 'Sheet1'] >>> # إنشاء ورقة جديدة في الفهرس 0 >>> wb.create_sheet(index=0, title='First Sheet') <Worksheet "First Sheet"> >>> wb.sheetnames ['First Sheet', 'Sheet', 'Sheet1'] >>> wb.create_sheet(index=2, title='Middle Sheet') <Worksheet "Middle Sheet"> >>> wb.sheetnames ['First Sheet', 'Sheet', 'Middle Sheet', 'Sheet1'] يعيد التابع create_sheet() كائن Worksheet جديد اسمه SheetX، والذي ضُبِط افتراضيًا ليكون الورقة الأخيرة في المصنف. يمكن اختياريًا تحديد فهرس واسم الورقة الجديدة باستخدام وسطاء الكلمات المفتاحية Keyword Arguments التي هي index و title. لنتابع المثال السابق من خلال كتابة ما يلي: >>> wb.sheetnames ['First Sheet', 'Sheet', 'Middle Sheet', 'Sheet1'] >>> del wb['Middle Sheet'] >>> del wb['Sheet1'] >>> wb.sheetnames ['First Sheet', 'Sheet'] يمكنك استخدام العامل del لحذف ورقة من مصنف، وهذا يماثل استخدامه لحذف زوج مفتاح-قيمة من القاموس. ملاحظة: تذكّر استدعاء التابع save() لحفظ التغييرات بعد إضافة أوراق إلى المصنف أو حذفها منه. كتابة القيم في الخلايا تشبه كتابة القيم في الخلايا إلى حدٍ كبير كتابة القيم في المفاتيح الموجودة ضمن القاموس. إذًا لندخل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb['Sheet'] >>> sheet['A1'] = 'Hello, world!' # تعديل قيمة الخلية >>> sheet['A1'].value 'Hello, world!' إذا كان لديك إحداثيات الخلية كسلسلة نصية، فيمكنك استخدامها بالطريقة نفسها لاستخدام مفتاح القاموس في الكائن Worksheet لتحديد الخلية التي تريد الكتابة فيها. تطبيق عملي: تحديث جدول بيانات ستكتب في هذا التطبيق العملي برنامجًا لتحديث الخلايا في جدول بيانات مبيعات المنتجات، حيث سيبحث برنامجك في جدول البيانات، ويعثر على أنواع معينة من المنتجات، ويحدّث أسعارها. يمكنك تنزيل جدول البيانات الخاص بهذا التطبيق العملي، ويوضح الشكل التالي كيف يبدو جدول البيانات: جدول بيانات مبيعات المنتجات يمثّل كل صف في هذا الجدول عملية بيع واحدة، والأعمدة هي نوع المنتج المباع (A)، والتكلفة لكل كيلوجرام من هذا المنتج (B)، وعدد الكيلوجرامات المباعة (C)، وإجمالي الإيرادات من عمليات البيع (D). يُضبَط عمود "الإجمالي TOTAL" على صيغة إكسل =ROUND(B3*C3, 2) التي تضرب تكلفة كل كيلوجرام بعدد الكيلوجرامات المُباعة وتقريب النتيجة إلى أقرب سنت. ستحدّث الخلايا الموجودة في العمود TOTAL نفسها تلقائيًا باستخدام هذه الصيغة في حالة وجود تغيير في العمود B أو C. لنفترض إدخال أسعار الثوم والكرفس والليمون بصورة غير صحيحة، مما يتركك أمام مهمة مملة تتمثل في المرور على آلاف الصفوف في جدول البيانات لتحديث تكلفة الكيلوجرام الواحد لصفوف الثوم Garlic والكرفس Celery والليمون Lemon. لا يمكنك إجراء عملية بحث واستبدال "find-and-replace" بسيطة للسعر بسبب وجود عناصر أخرى لها السعر نفسه ولا نريد تغييرها بصورة خاطئة. قد يستغرق تنفيذ ذلك يدويًا ساعات بالنسبة لآلاف الصفوف، ولكن يمكنك كتابة برنامج يمكنه إنجاز ذلك في ثوانٍ، حيث يطبّق برنامجك ما يلي: يتكرر على جميع الصفوف ضمن حلقة. إذا كان الصف للثوم أو الكرفس أو الليمون، فسيغيِّر السعر. وهذا يعني أن شيفرتك البرمجية يجب أن تطبّق الخطوات التالية: فتح ملف جدول البيانات. التحقق مما إذا كانت القيمة الموجودة في العمود A هي الكرفس Celery أو الثوم Garlic أو الليمون Lemon لجميع الصفوف. إذا كان الأمر كذلك، فسيتحدّث السعر في العمود B. حفظ جدول البيانات في ملف جديد حتى لا تفقد جدول البيانات القديم. الخطوة الأولى: إعداد هيكل البيانات باستخدام معلومات التحديث إليك الأسعار التي يجب تحديثها: المنتج السعر الكرفس Celery 1.19 الثوم Garlic 3.07 الليمون Lemon 1.27 ويمكنك كتابة الشيفرة البرمجية التالية: if produceName == 'Celery': cellObj = 1.19 if produceName == 'Garlic': cellObj = 3.07 if produceName == 'Lemon': cellObj = 1.27 يُعَد الحصول على بيانات المنتج والأسعار المُحدَّثة الثابتة باستخدام الطريقة السابقة أمرًا غير مناسب إلى حدٍ ما، فإذا كنت بحاجة إلى تحديث جدول البيانات مرة أخرى بأسعار مختلفة أو بمنتجات مختلفة، فيجب عليك تغيير الكثير من الشيفرة البرمجية، وبالتالي ستخاطر بإدخال أخطاء في كل مرة تغيّر فيها شيفرتك البرمجية. يتمثّل الحل الأفضل في تخزين معلومات السعر المُصحَّحة في قاموس وكتابة شيفرتك البرمجية لاستخدام هيكل البيانات، لذا أدخِل الشيفرة التالية في تبويب جديد لإنشاء ملف جديد في محرّرك: #! python3 # updateProduce.py - تصحيح أسعار المنتجات في جدول بيانات المبيعات import openpyxl wb = openpyxl.load_workbook('produceSales.xlsx') sheet = wb['Sheet'] # أنواع المنتجات وأسعارها المُحدَّثة PRICE_UPDATES = {'Garlic': 3.07, 'Celery': 1.19, 'Lemon': 1.27} # التكرار على جميع الصفوف وتحديث الأسعار احفظ الملف بالاسم updateProduce.py. إذا أردتَ تحديث جدول البيانات مرة أخرى، فيجب تحديث القاموس PRICE_UPDATES فقط دون تغيير أي شيفرة برمجية أخرى. الخطوة الثانية: التحقق من كافة الصفوف وتحديث الأسعار غير الصحيحة سيتكرر الجزء التالي من البرنامج على كافة الصفوف الموجودة في جدول البيانات، لذا أضِف الشيفرة البرمجية التالية إلى نهاية الملف updateProduce.py: #! python3 # updateProduce.py - تصحيح أسعار المنتجات في جدول بيانات المبيعات --snip-- # التكرار على جميع الصفوف وتحديث الأسعار ➊ for rowNum in range(2, sheet.max_row): # تخطي الصف الأول ➋ produceName = sheet.cell(row=rowNum, column=1).value ➌ if produceName in PRICE_UPDATES: sheet.cell(row=rowNum, column=2).value = PRICE_UPDATES[produceName] ➍ wb.save('updatedProduceSales.xlsx') نكرّر الشيفرة البرمجية على الصفوف بدءًا من الصف 2، لأن الصف 1 هو ترويسة الجدول ➊، ونخزّن الخلية الموجودة في العمود 1 (أي العمود A) في المتغير produceName ➋، حيث إذا كان هذا المتغير موجودًا بوصفه مفتاحًا في قاموس PRICE_UPDATES ➌، فيجب أن تعلم أن هذا الصف يجب تصحيح سعره، وسيكون السعر الصحيح في PRICE_UPDATES[produceName]. لاحظ مدى نظافة شيفرتك باستخدام قاموس PRICE_UPDATES، إذ لا توجد سوى تعليمة if واحدة فقط بدلًا من استخدام شيفرة تحتوي التعليمة if produceName == 'Garlic': مثلًا التي تكون ضرورية لكل نوعٍ من المنتجات يجب تحديثه. بما أن هذه الشيفرة البرمجية تستخدم قاموس PRICE_UPDATES بدلًا من كتابة شيفرة ثابتة لأسماء المنتجات وأسعارها المُحدَّثة ضمن حلقة for، فهذا يعني أنك ستعدّل قاموس PRICE_UPDATES فقط دون تعديل الشيفرة البرمجية، إذا احتاج جدول بيانات مبيعات المنتجات تغييراتٍ إضافية. تحفظ الشيفرة البرمجية كائن Workbook في الملف updatedProduceSales.xlsx ➍ بعد المرور على جدول البيانات بأكمله وإجراء التغييرات، ولا تكتب معلوماتٍ جديدة مكان معلومات جدول البيانات القديم إذا احتوى برنامجك خطأً وكان جدول البيانات المُحدَّث خاطئًا. يمكنك حذف جدول البيانات القديم بعد التحقق من أن جدول البيانات المُحدَّث صحيحًا. ملاحظة: لا تنسَ أنه يمكنك تنزيل الشيفرة المصدرية الكاملة لهذا البرنامج. أفكار لبرامج مماثلة يستخدم العديد من العاملين في وظائف مكتبية جداولَ بيانات إكسل طوال الوقت، لذا قد يكون البرنامج الذي يمكنه تعديل ملفات إكسل وكتابتها تلقائيًا مفيدًا جدًا لهم، إذ يمكن أن يفعل مثل هذا البرنامج الأمور التالية: قراءة البيانات من جدول بيانات واحد وكتابتها في أجزاء من جداول بيانات أخرى. قراءة البيانات من مواقع الويب أو الملفات النصية أو الحافظة وكتابتها في جدول بيانات. تنظيف البيانات تلقائيًا في جداول البيانات، فمثلًا يمكنه استخدام التعابير النمطية Regular Expressions لقراءة تنسيقات متعددة لأرقام الهواتف وتعديلها إلى تنسيق معياري واحد. ضبط نمط الخط Font Style في الخلايا يمكن أن يساعدك تنسيق خلايا أو صفوف أو أعمدة معينة في إبراز المناطق المهمة في جدول البيانات، إذ يمكن لبرنامجك في جدول بيانات المنتجات مثلًا تطبيق نص عريض على صفوف البطاطا Potato والثوم Garlic والجزر الأبيض Parsnip، أو يمكنه كتابة الصفوف التي تكلفة الكيلوجرام الواحد منها أكبر من 5 دولارات بخط مائل. قد يكون تنسيق أجزاء من جدول بيانات كبير أمرًا مملًا يدويًا، ولكن يمكن لبرامجك تطبيق ذلك مباشرةً. يمكنك تخصيص أنماط الخطوط في الخلايا من خلال استيراد الدالة Font() من الوحدة openpyxl.styles، مما يتيح لك كتابة Font() بدلًا من openpyxl.styles.Font() اختصارًا: from openpyxl.styles import Font ينشئ المثال التالي مصنفًا جديدًا ويضبط الخلية A1 ليكون الخط فيها خطًا مائلًا وبحجم 24 نقطة. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> from openpyxl.styles import Font >>> wb = openpyxl.Workbook() >>> sheet = wb['Sheet'] ➊ >>> italic24Font = Font(size=24, italic=True) # إنشاء الخط ➋ >>> sheet['A1'].font = italic24Font # تطبيق الخط في الخلية A1 >>> sheet['A1'] = 'Hello, world!' >>> wb.save('styles.xlsx') تعيد الدالة Font(size=24, italic=True) كائن Font المُخزَّن في المتغير italic24Font ➊، وتضبط وسطاء الكلمات المفتاحية الخاصة بالدالة Font() -مثل الوسيطين size و italic- معلومات تنسيق الكائن Font، وإذا أسندنا الكائن italic24Font ➋ إلى sheet['A1'].font، فستُطبَّق جميع معلومات تنسيق الخط على الخلية A1. كائنات الخط Font يمكن ضبط السمات font من خلال تمرير وسطاء الكلمات المفتاحية إلى الدالة Font()، حيث يوضّح الجدول التالي وسطاء الكلمات المفتاحية المُحتمَلة للدالة Font(): وسيط الكلمة المفتاحية نوع البيانات الوصف name سلسلة نصية اسم الخط مثل 'Calibri' أو 'Times New Roman' size عدد صحيح حجم الخط مقاسًا بالنقاط bold قيمة منطقية قيمته True إذا كان الخط عريضًا italic قيمة منطقية قيمته True إذا كان الخط مائلًا يمكنك استدعاء الدالة Font() لإنشاء كائن Font وتخزينه في متغير، ثم تسند السمة font الخاصة بكائن Cell إلى هذا المتغير، فمثلًا تنشئ الشيفرة البرمجية التالية تنسيقات خطوط مختلفة: >>> import openpyxl >>> from openpyxl.styles import Font >>> wb = openpyxl.Workbook() >>> sheet = wb['Sheet'] >>> fontObj1 = Font(name='Times New Roman', bold=True) >>> sheet['A1'].font = fontObj1 >>> sheet['A1'] = 'Bold Times New Roman' >>> fontObj2 = Font(size=24, italic=True) >>> sheet['B3'].font = fontObj2 >>> sheet['B3'] = '24 pt Italic' >>> wb.save('styles.xlsx') نخزّن كائن Font في المتغير fontObj1 الذي نسنده إلى السمة font الخاصة بالكائن Cell للخلية A1، ونكرر العملية نفسها مع كائن خط آخر لضبط خط الخلية الثانية. إذا نفّذنا الشيفرة البرمجية السابقة، فسيُضبَط تنسيق الخلايا A1 و B3 في جدول البيانات على تنسيقات الخطوط المُخصَّصة، وستبدو كما يلي: جدول بيانات يحتوي على أنماط خطوط مُخصَّصة ضبطنا اسم الخط في الخلية A1 على القيمة 'Times New Roman' والوسيط bold على القيمة true، حيث يظهر النص بالخط Times New Roman العريض، ولكننا لم نحدد حجم الخط، لذلك اُستخدِم الحجم الافتراضي للوحدة openpyxl، وهو الحجم 11. استخدمنا الخط المائل وبحجم 24 في الخلية B3، ولكن لم نحدد اسم الخط، لذلك اُستخدِم الخط الافتراضي للوحدة openpyxl، وهو الخط Calibri. صيغ إكسل تضبط صيغ إكسل -التي تبدأ بإشارة يساوي- الخلايا لتحتوي على قيم ناتجة عن تطبيق عمليات حسابية على خلايا أخرى، لذا سنستخدم في هذه الفقرة وحدة openpyxl لإضافة الصيغ إلى الخلايا برمجيًا مثل أيّ قيمة عادية أخرى كما يلي: >>> sheet['B9'] = '=SUM(B1:B8)' ستؤدي التعليمة السابقة إلى تخزين الصيغة =SUM(B1:B8) بوصفها قيمةً في الخلية B9، مما يؤدي إلى ضبط الخلية B9 على صيغة تحسب مجموع القيم في الخلايا من B1 إلى B8 كما في الشكل التالي: تحتوي الخلية B9 على الصيغة =SUM(B1:B8) التي تجمع القيم الموجودة في الخلايا من B1 إلى B8 تُضبَط صيغ إكسل مثل أيّ قيمة نصية أخرى في الخلية. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb.active >>> sheet['A1'] = 200 >>> sheet['A2'] = 300 >>> sheet['A3'] = '=SUM(A1:A2)' # ضبط الصيغة >>> wb.save('writeFormula.xlsx') ضبطنا الخلايا A1 و A2 على القيمتين 200 و 300 على التوالي، وضبطنا القيمة الموجودة في الخلية A3 على صيغة تجمع القيم الموجودة في الخليتين A1 و A2، وبالتالي ستظهر قيمة الخلية A3 على أنها 500 عند فتح جدول البيانات في إكسل. توفر صيغ إكسل مستوًى مقبولًا من البرمجة لجداول البيانات، ولكن تصبح هذه الصيغ غير قابلة للإدارة بسرعة بالنسبة للمهام المعقدة، فمثلًا حتى لو كنت على دراية كبيرة بصيغ إكسل، فمن الصعب محاولة تفسير ما تفعله الصيغة التالية: =IFERROR(TRIM(IF(LEN(VLOOKUP(F7, Sheet2!$A$1:$B$10000, 2, FALSE))>0,SUBSTITUTE(VLOOKUP(F7, Sheet2!$A$1:$B$10000, 2, FALSE), " ", ""),"")), "") لاحظ أن شيفرة بايثون البرمجية أكثر قابلية للقراءة من الصيغة السابقة. تعديل الصفوف والأعمدة يُعَد تعديل أحجام الصفوف والأعمدة في برنامج إكسل أمرًا سهلًا مثل النقر على حواف ترويسة الصف أو العمود وسحبها، ولكن إذا أردتَ ضبط حجم صف أو عمود بناءً على محتويات خلاياه أو إذا أردتَ ضبط الأحجام في عدد كبير من ملفات جداول البيانات، فستكون كتابة برنامج بايثون لذلك أسرع بكثير. يمكن أيضًا إخفاء الصفوف والأعمدة بصورة كاملة، أو يمكن تثبيتها بحيث تكون مرئية دائمًا على الشاشة وتظهر في جميع الصفحات عند طباعة جدول البيانات، إذ يُعَد ذلك مفيدًا للعناوين. ضبط ارتفاع الصف وعرض العمود تمتلك كائنات Worksheet سمات row_dimensions و column_dimensions التي تتحكم في ارتفاع الصفوف وعرض الأعمدة. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb.active >>> sheet['A1'] = 'Tall row' >>> sheet['B2'] = 'Wide column' >>> # ضبط الارتفاع والعرض >>> sheet.row_dimensions[1].height = 70 >>> sheet.column_dimensions['B'].width = 20 >>> wb.save('dimensions.xlsx') تُعَد السمات row_dimensions و column_dimensions الخاصة بالورقة قيمًا تشبه القاموس، إذ تحتوي السمة row_dimensions على كائنات RowDimension وتحتوي السمة column_dimensions على كائنات ColumnDimension. يمكنك الوصول إلى أحد الكائنات باستخدام رقم الصف (في مثالنا 1 أو 2) في row_dimensions، ويمكنك الوصول إلى أحد الكائنات باستخدام حرف العمود (في مثالنا A أو B) في column_dimensions. يبدو جدول البيانات dimensions.xlsx كما يلي: ضبطنا الصف 1 والعمود B على ارتفاع وعرض أكبر يمكنك ضبط ارتفاع الكائن RowDimension بعد الحصول عليه، ويمكنك ضبط عرض الكائن ColumnDimension بعد الحصول عليه. يمكن ضبط ارتفاع الصف ليكون عددًا صحيحًا أو عشريًا قيمته بين 0 و 409، إذ تمثّل هذه القيمة الارتفاع المُقاس بالنقاط، حيث تساوي النقطة الواحدة 1/72 من البوصة، ويكون ارتفاع الصف الافتراضي 12.75. يمكن ضبط عرض العمود ليكون عددًا صحيحًا أو عشريًا قيمته بين 0 و255، إذ تمثّل هذه القيمة عدد المحارف التي يمكن عرضها في الخلية بحجم الخط الافتراضي (11 نقطة)، ويكون عرض العمود الافتراضي 8.43 محرفًا. تُخفَى الأعمدة التي يبلغ عرضها 0 أو الصفوف التي يبلغ ارتفاعها 0 عن المستخدم. دمج وإلغاء دمج الخلايا يمكن دمج منطقة مستطيلة من الخلايا في خلية واحدة باستخدام التابع merge_cells() الخاص بالورقة. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb.active >>> sheet.merge_cells('A1:D3') # دمج جميع هذه الخلايا >>> sheet['A1'] = 'Twelve cells merged together.' >>> sheet.merge_cells('C5:D5') # دمج هاتين الخليتين >>> sheet['C5'] = 'Two merged cells.' >>> wb.save('merged.xlsx') وسيط التابع merge_cells() هو سلسلة نصية واحدة من الخلايا العلوية اليسرى والسفلية اليمنى للمنطقة المستطيلة المُراد دمجها، حيث تدمج 'A1:D3' اثنتا عشر خلية في خلية واحدة، ويمكنك ضبط قيمة هذه الخلايا المدموجة من خلال ضبط قيمة الخلية العلوية اليسرى لمجموعة الخلايا المدموجة. سيبدو الملف merged.xlsx كما يلي عند تشغيل الشيفرة البرمجية السابقة: الخلايا المدموجة في جدول البيانات يمكن إلغاء دمج الخلايا من خلال استدعاء التابع unmerge_cells() الخاص بالورقة. إذًا لندخِل ما يلي الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('merged.xlsx') >>> sheet = wb.active >>> sheet.unmerge_cells('A1:D3') # فصل هذه الخلايا عن بعضها >>> sheet.unmerge_cells('C5:D5') >>> wb.save('merged.xlsx') إذا حفظتَ تغييراتك ثم ألقيتَ نظرة على جدول البيانات، فسترى أن الخلايا المدموجة عادت إلى كونها خلايا مفردة. تثبيت الأجزاء من المفيد تثبيت عددٍ من الصفوف العلوية أو الأعمدة الموجودة في أقصى اليسار على الشاشة في جداول البيانات الكبيرة جدًا التي لا يمكن عرضها كاملة، فمثلًا تكون ترويسات الأعمدة أو الصفوف المُثبَّتة مرئيةً للمستخدم دائمًا حتى أثناء التمرير في جدول البيانات، ويُعرَف ذلك بتثبيت الأجزاء Freeze Panes. يحتوي كل كائن Worksheet في وحدة OpenPyXL على السمة freeze_panes التي يمكن ضبطها على كائن Cell أو سلسلة نصية من إحداثيات الخلية. لاحظ تثبيت كافة الصفوف الموجودة أعلى هذه الخلية وجميع الأعمدة الموجودة على يسارها، ولكن لن يُثبَّت صف وعمود الخلية نفسها. يمكن إلغاء تثبيت جميع الأجزاء من خلال ضبط السمة freeze_panes على القيمة None أو 'A1'. يوضح الجدول التالي الصفوف والأعمدة التي ستُثبَّت في بعض الأمثلة عند ضبط قيمة السمة freeze_panes: ضبط السمة freeze_panes الصفوف والأعمدة المُثبَّتة sheet.freeze_panes = 'A2' الصف 1 sheet.freeze_panes = 'B1' العمود A sheet.freeze_panes = 'C1' العمودان A و B sheet.freeze_panes = 'C2' الصف 1 والعمودان A و B sheet.freeze_panes = 'A1' أو sheet.freeze_panes = None لا توجد أجزاء مُثبَّتة تأكّد من حصولك على جدول بيانات مبيعات المنتجات، ثم أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('produceSales.xlsx') >>> sheet = wb.active >>> sheet.freeze_panes = 'A2' # تثبيت الصفوف الموجودة أعلى الصف A2 >>> wb.save('freezeExample.xlsx') إذا ضبطتَ السمة freeze_panes على القيمة 'A2'، فسيُعرَض الصف 1 دائمًا بغض النظر عن المكان الذي ينتقل إليه المستخدم عند التمرير في جدول البيانات، ويمكنك رؤية ذلك في الشكل التالي: يكون الصف 1 مرئيًا دائمًا حتى عندما يمرّر المستخدم جدول البيانات إلى الأسفل، إذا ضبطنا السمة freeze_panes على القيمة 'A2' المخططات Charts تدعم الوحدة OpenPyXL إنشاء مخططات شريطية وخطية ومبعثرة ودائرية باستخدام البيانات الموجودة في خلايا الورقة، حيث يمكنك إنشاء مخطط باتباع الخطوات التالية: إنشاء كائن Reference من خلايا المنطقة المستطيلة المُحدّدة. إنشاء كائن Series من خلال تمرير الكائن Reference. إنشاء كائن Chart. إلحاق كائن Series بكائن Chart. إضافة الكائن Chart إلى الكائن Worksheet مع تحديد الخلية التي يجب أن تكون في الزاوية العلوية اليسرى من المخطط اختياريًا. يمكنك إنشاء كائنات Reference من خلال استدعاء الدالة openpyxl.chart.Reference() وتمرير ثلاثة وسطاء هي: كائن Worksheet الذي يحتوي على بيانات مخططك. مجموعة Tuple مكونة من عددين صحيحين، حيث تمثل هذه المجموعة الخلية العلوية اليسرى من خلايا المنطقة المستطيلة المُحدَّدة التي تحتوي على بيانات مخططك، ويمثل العددُ الصحيح الأول في المجموعة الصفَّ، ويمثل العدد الصحيح الثاني العمود. لاحظ أن العدد 1 هو الصف الأول وليس العدد 0. مجموعة مكونة من عددين صحيحين، حيث تمثل هذه المجموعة الخلية السفلية اليمنى من خلايا المنطقة المستطيلة المُحدَّدة التي تحتوي على بيانات مخططك، ويمثل العدد الصحيح الأول في المجموعة الصف، ويمثل العدد الصحيح الثاني العمود. يوضح الشكل التالي بعض النماذج من وسطاء الإحداثيات: وهي من اليسار إلى اليمين: (1, 1), (10, 1); (3, 2), (6, 4); (5, 3), (5, 3) أدخِل مثال الصدفة التفاعلية التالي لإنشاء مخطط شريطي وإضافته إلى جدول البيانات: >>> import openpyxl >>> wb = openpyxl.Workbook() >>> sheet = wb.active >>> for i in range(1, 11): # إنشاء بعض البيانات في العمود A ... sheet['A' + str(i)] = i ... >>> refObj = openpyxl.chart.Reference(sheet, min_col=1, min_row=1, max_col=1, max_row=10) >>> seriesObj = openpyxl.chart.Series(refObj, title='First series') >>> chartObj = openpyxl.chart.BarChart() >>> chartObj.title = 'My Chart' >>> chartObj.append(seriesObj) >>> sheet.add_chart(chartObj, 'C5') >>> wb.save('sampleChart.xlsx') وينتج عن ذلك جدول بيانات يشبه ما يلي: جدول بيانات مع مخطط مضافٍ إليه أنشأنا مخططًا شريطيًا من خلال استدعاء الدالة openpyxl.chart.BarChart()، ويمكنك إنشاء مخططات خطية ومخططات مبعثرة ومخططات دائرية من خلال استدعاء الدوال openpyxl.charts.LineChart() و openpyxl.charts.LineChart() و openpyxl.charts.LineChart(). مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي، لكسب خبرة عملية أكبر. برنامج لإنشاء جدول الضرب أنشئ برنامج multiplicationTable.py الذي يأخذ العدد N من سطر الأوامر وينشئ جدول الضرب N×N في جدول بيانات إكسل، فمثلًا إذا شغّلنا البرنامج كما يلي: py multiplicationTable.py 6 يجب أن ينشئ جدول بيانات يشبه الشكل التالي: توليد جدول الضرب في جدول بيانات يجب استخدام الصف 1 والعمود A للتسميات أو عناوين الصفوف والأعمدة ويجب أن يكونا بالخط العريض. برنامج لإدراج صف فارغ أنشئ برنامج blankRowInserter.py الذي يأخذ عددين صحيحين وسلسلة نصية لاسم الملف كوسطاء لسطر الأوامر، حيث نسمّي العدد الصحيح الأول N والعدد الصحيح الثاني M. يجب على البرنامج إدراج صفوف فارغة بعدد M في جدول البيانات بدءًا من الصف N، فمثلًا إذا شغّلنا البرنامج كما يلي: python blankRowInserter.py 3 2 myProduce.xlsx يجب أن تبدو جداول البيانات "قبل" و"بعد" الإدراج كما في الشكل التالي: قبل (يسار) وبعد (يمين) إدراج الصفين الفارغين عند الصف 3 يمكنك كتابة هذا البرنامج من خلال قراءة محتويات جدول البيانات، ثم استخدام حلقة for لنسخ الأسطر N الأولى عند كتابة جدول البيانات الجديد، وجمع العدد M مع رقم الصف في جدول البيانات الناتج بالنسبة للأسطر المتبقية. برنامج لعكس خلايا جدول البيانات اكتب برنامجًا لعكس الصف والعمود الخاص بالخلايا في جدول البيانات، فمثلًا ستكون القيمة في الصف 5 والعمود 3 موجودة في الصف 3 والعمود 5 والعكس صحيح، ويجب تطبيق ذلك على جميع الخلايا في جدول البيانات، إذ ستبدو جداول البيانات "قبل" و"بعد" العكس كما في الشكل التالي: جدول البيانات قبل (العلوي) وبعد (السفلي) يمكنك كتابة هذا البرنامج باستخدام حلقات for متداخلة لقراءة بيانات جدول البيانات في قائمة من قوائم هيكل البيانات، ويمكن أن يحتوي هيكل البيانات على sheetData[x][y] للخلية الموجودة في العمود x والصف y. استخدم بعد ذلك sheetData[y][x] للخلية الموجودة في العمود x والصف y عند كتابة جدول البيانات الجديد. برنامج لتحويل الملفات النصية إلى جدول بيانات اكتب برنامجًا لقراءة محتويات العديد من الملفات النصية (يمكنك إنشاء الملفات النصية بنفسك) وإدراج هذه المحتويات في جدول بيانات، بحيث يقابل سطر واحد من الملف النصي صفًا واحدًا من جدول البيانات. ستكون أسطر الملف النصي الأول في خلايا العمود A، وستكون أسطر الملف النصي الثاني في خلايا العمود B، وهكذا. استخدم التابع readlines() الخاص بالكائن File لإعادة قائمة من السلاسل النصية، حيث تقابل كل سلسلة نصية واحدة سطرًا في الملف. يكون السطر الأول من الملف الأول مقابلًا للعمود 1 والصف 1، ويجب كتابة السطر الثاني في العمود 1 والصف 2، وما إلى ذلك. سيُكتَب الملف التالي المقروء باستخدام التابع readlines() في العمود 2، وسيُكتَب الملف الذي يليه في العمود 3، وهكذا. برنامج لتحويل جدول بيانات إلى ملفات نصية اكتب برنامجًا يؤدّي مهام البرنامج السابق بترتيب عكسي، إذ يجب أن يفتح البرنامج جدول بيانات ويكتب خلايا العمود A في ملف نصي واحد، وخلايا العمود B في ملف نصي آخر، وهكذا. الخلاصة لا يكون الجزء الصعب من معالجة المعلومات هو المعالجة بحد ذاتها في كثير من الأحيان، بل تتمثل الصعوبة ببساطة في الحصول على البيانات بالتنسيق الصحيح المناسب لبرنامجك، ولكن يمكنك استخراج بيانات جدول بياناتك ومعالجتها بصورة أسرع بكثير مما يمكنك فعله يدويًا بعد تحميله إلى ملف بايثون. يمكنك أيضًا توليد جداول بيانات بوصفها خرجًا لبرامجك، لذا إذا كان زملاؤك بحاجة إلى نقل ملفك النصي أو ملف PDF الذي يحتوي على آلاف جهات اتصال المبيعات إلى ملف جدول بيانات، فلن تضطر إلى نسخه ولصقه بالكامل في ملف إكسل. إذا كان لديك وحدة openpyxl وبعض المعرفة البرمجية، فستجد أن معالجة جداول البيانات الكبيرة تُعَد أمرًا سهلًا للغاية. سنلقي نظرة في المقال التالي على استخدام لغة بايثون للتفاعل مع برنامج آخر لجداول بيانات، وهو تطبيق جداول بيانات جوجل Google Sheets الشهير على الإنترنت. ترجمة -وبتصرُّف- للقسم Writing Excel Documents من مقال Working with Excel Spreadsheets لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: قراءة مستندات جداول إكسل باستخدام لغة بايثون Python استخراج البيانات من الويب عبر لغة بايثون Python قراءة وكتابة الملفات باستخدام لغة بايثون Python التعامل مع الملفات والمسارات في بايثون نظرة عامة على برنامج مايكروسوفت إكسل Microsoft Excel النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
-
قد لا نفكر في جداول البيانات بوصفها أدوات برمجية، ولكن يستخدمها الجميع تقريبًا لتنظيم المعلومات في هياكل بيانية ثنائية الأبعاد، وإجراء العمليات الحسابية باستخدام الصيغ، وعرض المخرجات على شكل مخططات، لذا سندمج لغة بايثون مع تطبيقين شائعين خاصين بجداول البيانات وهما: مايكروسوفت إكسل Microsoft Excel وجداول بيانات جوجل Google Sheets. يُعَد إكسل تطبيق جداول بيانات قوي وشائع الاستخدام لنظام تشغيل ويندوز، وتوجد وحدةٌ برمجية هي وحدة openpyxl التي تسمح لبرامجك المكتوبة بلغة بايثون بقراءة وتعديل ملفات جداول بيانات إكسل. يمكن أن تكون لديك مهمة مملة متمثلة في نسخ بيانات معينة من جدول بيانات ولصقها في جدول بيانات آخر مثلًا، أو قد يتعين عليك أن تمر على آلاف الصفوف واختيار عددٍ قليل منها لإجراء تعديلات بسيطة بناءً على بعض المعايير، أو قد يتعين عليك الاطلاع على مئات جداول البيانات الخاصة بميزانيات الأقسام باحثًا عن الخلايا الملونة باللون الأحمر، وهذه هي مهام جداول البيانات البسيطة والمملة التي يمكن لبايثون تطبيقها نيابةً عنك. يُعَد برنامج إكسل برنامجًا خاصًا بشركة مايكروسوفت، ولكن توجد بدائل مجانية تعمل على أنظمة تشغيل ويندوز وماك macOS ولينكس Linux، حيث يعمل كل من ليبرأوفيس كالك LibreOffice Calc وأوبن أوفيس كالك OpenOffice Calc بنجاح مع صيغة ملفات جداول البيانات .xlsx الخاصة بإكسل، مما يعني أن الوحدة openpyxl يمكن أن تعمل مع جداول البيانات الخاصة بهذين التطبيقين أيضًا، ويمكنك تنزيلهما من موقعهما الرسمي مباشرةً. قد تجد أن هذين البرنامجين أسهل في الاستخدام من إكسل بالرغم من أن برنامج إكسل مُثبَّتٌ مسبقًا على حاسوبك الشخصي، ولكن لقطات الشاشة الموجودة في هذا المقال جميعها مأخوذة من إكسل 2010 على نظام تشغيل ويندوز 10. مستندات إكسل لنتعرّف أولًا على بعض التعريفات الأساسية، حيث يُسمَّى مستند جدول بيانات إكسل بالمصنف Workbook، ويُحفَظ المصنف في ملف امتداده .xlsx، ويمكن أن يحتوي المصنف على أوراق Sheets متعددة (وتسمَّى أوراق عمل Worksheets أيضًا)، كما تُسمَّى الورقة التي يعرضها المستخدم حاليًا -أو المعروضة آخر مرة قبل إغلاق إكسل- بالورقة النشطة Active Sheet. تحتوي كل ورقة على أعمدة Columns تتعامل معها باستخدام حروف تبدأ بالحرف A، وصفوف Rows تتعامل معها باستخدام أعداد تبدأ بالعدد 1. يسمى المربع الموجود في عمود أو صف معين بالخلية Cell، ويمكن أن تحتوي كل خلية على قيمة عددية أو نصية، وتتشكّل الورقة من شبكةٍ من الخلايا التي تحتوي على البيانات. تثبيت وحدة openpyxl لا تحتوي لغة بايثون مسبقًا على وحدة OpenPyXL، إذ يجب عليك تثبيتها أولًا، لذا اتبع الإرشادات الخاصة بتثبيت الوحدات الخارجية التي سنوضحها في مقال لاحق. سنستخدم في هذا المقال الإصدار 2.6.2 من الوحدة OpenPyXL، لذا من المهم أن تثبّت هذا الإصدار من خلال تشغيل الأمرpip install --user -U openpyxl==2.6.2، لأن الإصدارات الأحدث منها غير متوافقة مع المعلومات الموجودة في هذا المقال. أدخِل الأمر التالي في الصدفة التفاعلية Interactive Shell لاختبار ما إذا كان كانت وحدة OpenPyXL مُثبَّتة بصورة صحيحة: >>> import openpyxl إذا جرى تثبيت الوحدة بصورة صحيحة، فلن ينتج عن الأمر السابق أيّ رسائل خطأ. تذكّر استيراد الوحدة openpyxl قبل تشغيل أمثلة أوامر الصدفة التفاعلية في هذا المقال، وإلّا فستحصل على الخطأ NameError: name 'openpyxl' is not defined. ملاحظة: يمكنك العثور على توثيق وحدة OpenPyXL الكامل على موقعها الرسمي. قراءة مستندات إكسل سنستخدم في الأمثلة الواردة في هذا المقال جدول بيانات اسمه example.xlsx مُخزَّن في المجلد الجذر، حيث يمكنك إما إنشاء جدول البيانات بنفسك أو تنزيله. يوضّح الشكل التالي تبويبات للأوراق الافتراضية الثلاثة التي اسمها Sheet1 و Sheet2 و Sheet3 التي يوفرها إكسل تلقائيًا للمصنفات الجديدة، ولكن قد يختلف عدد هذه الأوراق الافتراضية بحسب نظام التشغيل وبرنامج جداول البيانات: توجد التبويبات الخاصة بأوراق المصنف في الزاوية السفلية اليسرى من إكسل يجب أن تبدو الورقة 1 في مثالنا مثل الجدول التالي، ولكن إن لم تنزّل الملف example.xlsx من الموقع، فيجب أن تدخِل هذه البيانات في الورقة بنفسك: A B C 1 4/5/2015 1:34:02 PM Apples 73 2 4/5/2015 3:41:23 AM Cherries 85 3 4/6/2015 12:46:51 PM Pears 14 4 4/8/2015 8:59:43 AM Oranges 52 5 4/10/2015 2:07:00 AM Apples 152 6 4/10/2015 6:10:37 PM Bananas 23 7 4/10/2015 2:40:46 AM Strawberries 98 أصبح جدول البيانات جاهزًا الآن، إذًا لنتعرّف على كيفية التعامل معه باستخدام وحدة openpyxl. فتح مستندات إكسل باستخدام وحدة OpenPyXL يمكنك استخدام الدالة openpyxl.load_workbook() بعد استيراد وحدة openpyxl مباشرةً، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> type(wb) <class 'openpyxl.workbook.workbook.Workbook'> تأخذ الدالة openpyxl.load_workbook() اسم الملف وتعيد قيمة نوع بيانات المصنف workbook، حيث يمثل الكائن Workbook ملف إكسل، ويشبه ذلك كيفية تمثيل الكائن File ملفًا نصيًا مفتوحًا. تذكّر أن الملف example.xlsx يجب أن يكون موجودًا في مجلد العمل الحالي لتتمكّن من التعامل معه، إذ يمكنك معرفة مجلد العمل الحالي من خلال استيراد وحدة os واستخدام الدالة os.getcwd()، ويمكنك تغيير مجلد العمل الحالي باستخدام الدالة os.chdir(). الحصول على الأوراق من المصنف يمكنك الحصول على قائمة بجميع أسماء الأوراق في المصنف من خلال الوصول إلى السمة Attribute التي هي sheetnames، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> wb.sheetnames # أسماء الأوراق الخاصة بالمصنف ['Sheet1', 'Sheet2', 'Sheet3'] >>> sheet = wb['Sheet3'] # الحصول على ورقة من المصنف >>> sheet <Worksheet "Sheet3"> >>> type(sheet) <class 'openpyxl.worksheet.worksheet.Worksheet'> >>> sheet.title # الحصول على عنوان الورقة بوصفه سلسلة نصية 'Sheet3' >>> anotherSheet = wb.active # الحصول على الورقة النشطة >>> anotherSheet <Worksheet "Sheet1"> يمثّل الكائنُ Worksheet الورقة التي يمكنك الحصول عليها باستخدام الأقواس المربعة مع السلسلة النصية التي تمثّل اسم هذه الورقة، حيث يشبه ذلك استخدام مفتاح القاموس، ويمكنك استخدام السمة active للكائن Workbook للحصول على ورقة المصنف النشطة، فالورقة النشطة هي الورقة الموجودة في المقدمة عند فتح المصنف في إكسل. كما يمكنك الحصول على اسم الكائن Worksheet من سمة العنوان title بعد الحصول على هذا الكائن. الحصول على الخلايا من الأوراق يمكنك الوصول إلى الكائن Cell باستخدام اسمه بعد الحصول على الكائن Worksheet، لذا أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb['Sheet1'] # الحصول على ورقة من المصنف >>> sheet['A1'] # الحصول على خلية من الورقة <Cell 'Sheet1'.A1> >>> sheet['A1'].value # الحصول على القيمة من الخلية datetime.datetime(2015, 4, 5, 13, 34, 2) >>> c = sheet['B1'] # الحصول على خلية أخرى من الورقة >>> c.value 'Apples' >>> # الحصول على الصف والعمود والقيمة من الخلية >>> 'Row %s, Column %s is %s' % (c.row, c.column, c.value) 'Row 1, Column B is Apples' >>> 'Cell %s is %s' % (c.coordinate, c.value) 'Cell B1 is Apples' >>> sheet['C1'].value 73 يحتوي كائن الخلية Cell على سمة القيمة value التي تحتوي على القيمة المُخزَّنة في هذه الخلية، وتحتوي الكائنات Cell أيضًا على سمات الصف row والعمود column والإحداثيات coordinate التي توفّر معلومات موقع الخلية، فمثلًا يعطينا الوصول إلى السمة value للكائن Cell الخاص بالخلية B1 السلسلة النصية 'Apples'، وتعطينا السمة row العدد الصحيح 1، وتعطينا السمة column العمود 'B'، وتعطي السمة coordinate القيمة 'B1'. تفسّر الوحدة OpenPyXL تلقائيًا التواريخ الموجودة في العمود A وتعيدها بوصفها قيم datetime بدلًا من إعادتها كسلاسل نصية، حيث سنوضّح لاحقًا نوع البيانات datetime. يمكن أن يكون تحديد عمود باستخدام حرف أمرًا صعبًا برمجيًا، وخاصةً لأن الأعمدة تبدأ باستخدام حرفين مثل AA و AB و AC بعد العمود Z، لذا يمكنك بدلًا من ذلك الحصول على خلية باستخدام التابع cell() الخاص بالورقة وتمرير أعداد صحيحة لوسطاء الكلمات المفتاحية Keyword Arguments التي هي row و column الخاصة بهذا التابع، ويكون العدد الصحيح للصف أو العمود الأول هو 1 وليس 0. لندخِل ما يلي في الصدفة التفاعلية: sheet.cell(row=1, column=2) <Cell 'Sheet1'.B1> >>> sheet.cell(row=1, column=2).value 'Apples' >>> for i in range(1, 8, 2): # المرور على جميع الصفوف الأخرى ... print(i, sheet.cell(row=i, column=2).value) ... 1 Apples 3 Pears 5 Apples 7 Strawberries لاحظ أن استخدام التابع cell() الخاص بالورقة وتمرير row=1 و column=2 له يعطي كائن Cell للخلية B1 كما فعل استخدام sheet['B1'] تمامًا. يمكنك بعد ذلك كتابة حلقة for لطباعة قيم سلسلة من الخلايا باستخدام التابع cell() ووسطاء الكلمات المفتاحية الخاصة به. لنفترض أنك تريد الانتقال إلى العمود B وطباعة القيمة الموجودة في جميع الخلايا التي يكون رقم صفها عددًا فرديًا، حيث يمكنك الحصول على الخلايا لجميع الصفوف لها أرقام فردية من خلال تمرير القيمة 2 لمعامل "الخطوة step" الخاص بالدالة range(). يُمرَّر المتغير i الخاص بالحلقة for إلى وسيط الكلمة المفتاحية row الخاص بالتابع cell()، بينما تُمرّر القيمة 2 دائمًا إلى وسيط الكلمة المفتاحية column في هذه الحالة. لاحظ أننا مرّرنا العدد الصحيح 2 ولم نمرّر السلسلة النصية 'B'. يمكنك تحديد حجم الورقة باستخدام السمتين max_row و max_column للكائن Worksheet كما يلي: import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb['Sheet1'] >>> sheet.max_row # الحصول على عدد الصفوف الأكبر 7 >>> sheet.max_column # الحصول على عدد الأعمدة الأكبر 3 لاحظ أن السمة max_column تمثل عددًا صحيحًا ولا تمثّل الحرف الذي يظهر في إكسل. تحويل حروف الأعمدة إلى أعداد يمكنك تحويل أسماء الأعمدة من حروف إلى أعداد من خلال استدعاء الدالة openpyxl.utils.column_index_from_string()، ويمكنك التحويل من أعداد إلى حروف من خلال استدعاء الدالة openpyxl.utils.get_column_letter(). إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> from openpyxl.utils import get_column_letter, column_index_from_string >>> get_column_letter(1) # ترجمة العمود 1 إلى حرف 'A' >>> get_column_letter(2) 'B' >>> get_column_letter(27) 'AA' >>> get_column_letter(900) 'AHP' >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb['Sheet1'] >>> get_column_letter(sheet.max_column) 'C' >>> column_index_from_string('A') # الحصول على العدد المقابل للحرف A 1 >>> column_index_from_string('AA') 27 يمكنك استدعاء الدالة get_column_letter() وتمرير عدد صحيح لها مثل العدد 27 لمعرفة اسم الحرف في العمود السابع والعشرين بعد استيراد الدالتين السابقتين من الوحدة openpyxl.utils. بينما تطبّق الدالة column_index_string() العكس، حيث تمرّر لها الحرف الذي يمثل اسم العمود، وتعطيك رقم هذا العمود. لا تحتاج إلى تحميل مصنف لاستخدام هذه الدوال، ولكن إن أردت يمكنك تحميل مصنف، ثم الحصول على الكائن Worksheet واستخدام سمته max_column مثلًا للحصول على عدد صحيح، ثم يمكنك تمرير هذا العدد الصحيح إلى الدالة get_column_letter(). الحصول على الصفوف والأعمدة من الأوراق يمكنك تقسيم الكائنات Worksheet للحصول على كافة الكائنات Cell الموجودة في صف أو عمود أو منطقة مستطيلة من جدول البيانات، ثم يمكنك التكرار على جميع الخلايا الموجودة في كل قسم. أدخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb['Sheet1'] >>> tuple(sheet['A1':'C3']) # الحصول على جميع الخلايا من A1 إلى C3 ((<Cell 'Sheet1'.A1>, <Cell 'Sheet1'.B1>, <Cell 'Sheet1'.C1>), (<Cell 'Sheet1'.A2>, <Cell 'Sheet1'.B2>, <Cell 'Sheet1'.C2>), (<Cell 'Sheet1'.A3>, <Cell 'Sheet1'.B3>, <Cell 'Sheet1'.C3>)) ➊ >>> for rowOfCellObjects in sheet['A1':'C3']: ➋ ... for cellObj in rowOfCellObjects: ... print(cellObj.coordinate, cellObj.value) ... print('--- نهاية الصف ---') A1 2015-04-05 13:34:02 B1 Apples C1 73 --- نهاية الصف --- A2 2015-04-05 03:41:23 B2 Cherries C2 85 --- نهاية الصف --- A3 2015-04-06 12:46:51 B3 Pears C3 14 --- نهاية الصف — حدّدنا في المثال السابق أننا نريد كائنات Cell في المنطقة المستطيلة من الخلية A1 إلى الخلية C3، وحصلنا على كائن Generator الذي يحتوي على كائنات Cell في تلك المنطقة، حيث يمكننا تصوّر الكائن Generator من خلال استخدام الدالة tuple() معه لعرض كائنات Cell الخاصة به ضمن مجموعة Tuple. تحتوي هذه المجموعة على ثلاث مجموعات أخرى، مجموعة لكل صف من أعلى المنطقة المطلوبة إلى أسفلها، حيث تحتوي كل مجموعة من هذه المجموعات الداخلية الثلاث على كائنات Cell في صف واحد من المنطقة المطلوبة من الخلية الموجودة في أقصى اليسار إلى اليمين. يحتوي قسم الورقة الذي حدّدناه على جميع كائنات Cell في المنطقة المؤلفة من الخلية A1 إلى الخلية C3، بدءًا من الخلية العلوية اليسرى وانتهاءً بالخلية السفلية اليمنى. طبعنا قيم كل خلية في هذه المنطقة باستخدام حلقتي for، حيث تتكرر حلقة for الخارجية على كل صف في القسم ➊، ثم تتكرر حلقة for المتداخلة لكل صف على كل خلية في هذا الصف ➋. يمكن أيضًا الوصول إلى قيم الخلايا في صف أو عمود معين من خلال استخدام سمة rows و columns الخاصة بكائن Worksheet، ولكن يجب تحويل هذه السمات إلى قوائم باستخدام الدالة list() قبل أن تتمكّن من استخدام الأقواس المربعة والفهرس معها. إذًا لندخِل ما يلي في الصدفة التفاعلية: >>> import openpyxl >>> wb = openpyxl.load_workbook('example.xlsx') >>> sheet = wb.active >>> list(sheet.columns)[1] # الحصول على خلايا العمود الثاني (<Cell 'Sheet1'.B1>, <Cell 'Sheet1'.B2>, <Cell 'Sheet1'.B3>, <Cell 'Sheet1'. B4>, <Cell 'Sheet1'.B5>, <Cell 'Sheet1'.B6>, <Cell 'Sheet1'.B7>) >>> for cellObj in list(sheet.columns)[1]: print(cellObj.value) Apples Cherries Pears Oranges Apples Bananas Strawberries سيؤدي استخدام السمة rows مع الكائن Worksheet إلى إعطاء مجموعة من عدة مجموعات، وتمثل كل مجموعة من هذه المجموعات الداخلية صفًا، وتحتوي على كائنات Cell الموجودة في هذا الصف. تعطي السمة columns أيضًا مجموعة من عدة مجموعات، حيث تحتوي كل مجموعة من المجموعات الداخلية على كائنات Cell في عمود معين. لدينا في مثالنا الملف example.xlsx الذي يحتوي على 7 صفوف و3 أعمدة، وبالتالي تعطي السمة rows مجموعة مؤلفةً من 7 مجموعات، حيث تحتوي كل منها على 3 كائنات Cell، وتعطي السمة columns مجموعة مكونة من 3 مجموعات، حيث تحتوي كل منها على 7 كائنات Cell. يمكن الوصول إلى مجموعة معينة من خلال الإشارة إليها باستخدام فهرسها في المجموعة الكبرى، فمثلًا نحصل على المجموعة التي تمثل العمود B من خلال استخدام list(sheet.columns)[1]، ونحصل على المجموعة التي تحتوي على كائنات Cell في العمود A من خلال استخدام list(sheet.columns)[0]. يمكنك بعد أن يكون لديك مجموعة تمثل صفًا أو عمودًا واحدًا التكرار على كائنات Cell الخاصة بها وطباعة قيمها. المصنفات والأوراق والخلايا إليك ملخص بجميع الدوال والتوابع وأنواع البيانات المستخدمة في قراءة خلية من ملف جدول بيانات: استيراد وحدة openpyxl. استدعاء الدالة openpyxl.load_workbook(). الحصول على كائن Workbook. استخدام السمة active أو السمة sheetnames. الحصول على كائن Worksheet. استخدم طريقة الفهرسة أو تابع cell() الخاص بالورقة مع وسطاء الكلمات المفتاحية row و column. الحصول على كائن Cell. قراءة السمة value الخاصة بالكائن Cell. تطبيق عملي: قراءة البيانات من جدول بيانات لنفترض أن لديك جدول بيانات يمثّل الإحصاء السكاني للولايات المتحدة الأمريكية لعام 2010، ولديك مهمة مملة تتمثل في استعراض آلاف الصفوف لحساب إجمالي عدد السكان وعدد المناطق الإحصائية Census Tracts لكل مقاطعة County، فالمنطقة الإحصائية هي ببساطة منطقة جغرافية محددة لأغراض الإحصاء السكاني، ويمثل كل صف في جدول البيانات منطقةً إحصائية واحدة. سنسمّي ملف جدول البيانات censuspopdata.xlsx الذي يمكنك تنزيله، وتبدو محتوياته كما يلي: جدول بيانات censuspopdata.xlsx يستطيع إكسل حساب مجموع خلايا محددة متعددة، ولكن يجب عليك أيضًا تحديد الخلايا التي تمثل المقاطعات التي يزيد عدد سكانها عن 3000 نسمة. كما قد يستغرق حساب عدد سكان المقاطعة يدويًا بضع ثوانٍ فقط، ولكنه قد يستغرق ساعات لجدول البيانات بأكمله. ستكتب في هذا التطبيق العملي سكربتًا يمكنه القراءة من ملف جدول بيانات الإحصاء السكاني وحساب إحصائيات كل مقاطعة في غضون ثوانٍ، إذ سيفعل برنامجك ما يلي: يقرأ البيانات من جدول بيانات إكسل. يحسب عدد المناطق الإحصائية في كل مقاطعة. يحسب إجمالي عدد السكان في كل مقاطعة. يطبع النتائج. وهذا يعني أن شيفرتك البرمجية ستحتاج ما يلي: فتح وقراءة خلايا مستند إكسل باستخدام وحدة openpyxl. حساب جميع بيانات المناطق الإحصائية وعدد السكان وتخزينها في هيكل بيانات. كتابة هيكل البيانات في ملف نصي له الامتداد .py باستخدام الوحدة pprint. الخطوة الأولى: قراءة بيانات جدول البيانات توجد ورقة واحدة فقط في جدول البيانات censuspopdata.xlsx، تسمى "عدد السكان حسب المنطقة الإحصائية" 'Population by Census Tract'، ويحتوي كل صف على بيانات منطقة إحصائية واحدة، والأعمدة هي رقم المنطقة (A) واختصار الولاية (B) واسم المقاطعة (C) وعدد سكان المنطقة (D). افتح تبويبًا جديدًا لإنشاء ملف جديد في محرّرك وأدخِل الشيفرة البرمجية التالية، واحفظ الملف بالاسم readCensusExcel.py: #! python3 # readCensusExcel.py - جدول عدد السكان وعدد المناطق الإحصائية لكل مقاطعة ➊ import openpyxl, pprint print('Opening workbook...') ➋ wb = openpyxl.load_workbook('censuspopdata.xlsx') ➌ sheet = wb['Population by Census Tract'] countyData = {} # املأ بيانات المقاطعة countyData بعدد سكان كل مقاطعة ومناطقها الإحصائية print('Reading rows...') ➍ for row in range(2, sheet.max_row + 1): # يحتوي كل صف في جدول البيانات على بياناتٍ لمنطقة إحصائية واحدة state = sheet['B' + str(row)].value county = sheet['C' + str(row)].value pop = sheet['D' + str(row)].value # افتح ملفًا نصيًا جديدًا واكتب محتويات بيانات المقاطعة countyData فيه تستورد الشيفرة البرمجية السابقة الوحدة openpyxl والوحدة pprint التي ستستخدمها لطباعة بيانات المقاطعة النهائية ➊، ثم تفتح الملف censuspopdata.xlsx ➋، وتحصل على الورقة التي تحتوي على البيانات الإحصائية ➌، وتبدأ بالتكرار على صفوف هذه الورقة ➍. لاحظ أنك أنشأتَ أيضًا متغيرًا بالاسم countyData، والذي سيحتوي على عدد السكان وعدد المناطق التي تحسبها لكل مقاطعة، ولكن يجب عليك تحديد كيفية هيكلة البيانات بداخله قبل أن تتمكن من تخزين أيّ شيء فيه. الخطوة الثانية: ملء هيكل البيانات يُعَد هيكل البيانات المُخزَّن في المتغير countyData قاموسًا تكون اختصارات أسماء الولايات مفاتيحًا له، حيث سيُربَط اختصار كل ولاية مع قاموس آخر مفاتيحه هي سلاسل نصية تمثّل أسماء المقاطعات في تلك الولاية، وسيُربَط كل اسم مقاطعة بدوره مع قاموسٍ آخر يحتوي على مفتاحين فقط هما 'tracts' و 'pop'، ويُربَط هذان المفتاحان مع عدد المناطق الإحصائية وعدد السكان في المقاطعة، فمثلًا سيبدو القاموس مشابهًا لما يلي: {'AK': {'Aleutians East': {'pop': 3141, 'tracts': 1}, 'Aleutians West': {'pop': 5561, 'tracts': 2}, 'Anchorage': {'pop': 291826, 'tracts': 55}, 'Bethel': {'pop': 17013, 'tracts': 3}, 'Bristol Bay': {'pop': 997, 'tracts': 1}, --snip– إذا خُزِّن القاموس السابق في المتغير countyData، فيمكن تقييم التعابير التالية كما يلي: >>> countyData['AK']['Anchorage']['pop'] 291826 >>> countyData['AK']['Anchorage']['tracts'] 55 وستكون مفاتيح قاموس countyData كما يلي: countyData[state abbrev][county]['tracts'] countyData[state abbrev][county]['pop'] عرفتَ كيفية تنظيم هيكل بيانات countyData، ويمكنك الآن كتابة الشيفرة البرمجية التي ستملؤه ببيانات المقاطعة، لذا أضِف الشيفرة البرمجية التالية إلى نهاية برنامجك: #! python 3 # readCensusExcel.py - جدول عدد السكان وعدد المناطق الإحصائية لكل مقاطعة --snip-- for row in range(2, sheet.max_row + 1): # يحتوي كل صف في جدول البيانات على بيانات لمنطقة إحصائية واحدة state = sheet['B' + str(row)].value county = sheet['C' + str(row)].value pop = sheet['D' + str(row)].value # تأكد من وجود مفتاح هذه الولاية state ➊ countyData.setdefault(state, {}) # تأكد من وجود مفتاح هذه المقاطعة county في تلك الولاية ➋ countyData[state].setdefault(county, {'tracts': 0, 'pop': 0}) # يمثل كل صف منطقة إحصائية واحدة، لذا يجب زيادة عدد المناطق بمقدار واحد ➌ countyData[state][county]['tracts'] += 1 # زيادة عدد سكان pop المقاطعة بمقدار عدد السكان في هذه المنطقة الإحصائية ➍ countyData[state][county]['pop'] += int(pop) # افتح ملفًا نصيًا جديدًا واكتب محتويات بيانات المقاطعة countyData فيه يجري السطران الأخيران من الشيفرة البرمجية السابقة العمليات الحسابية الفعلية، حيث تزيد قيمة المناطق الإحصائية tracts ➌ وقيمة عدد السكان pop ➍ للمقاطعة الحالية في كل تكرار لحلقة for. بينما سببُ وجود الشيفرة البرمجية المتبقية هو أنه لا يمكنك إضافة قاموس المقاطعة بوصفه قيمةً لمفتاح اختصار الولاية إلّا عند وجود المفتاح نفسه في countyData، إذ ستتسبّب التعليمة countyData['AK']['Anchorage']['tracts'] += 1 في حدوث خطأ إن لم يكن المفتاح 'AK' موجودًا بعد. يمكنك التأكد من وجود مفتاح اختصار الولاية في هيكل بياناتك من خلال استدعاء التابع setdefault() لضبط قيمة الولاية state ➊ إن لم تكن موجودة مسبقًا. يحتاج قاموس countyData إلى قاموس آخر بوصفه قيمةً لكل مفتاح يمثّل اختصار الولاية، وبالتالي سيحتاج كلٌّ من هذه القواميس إلى قاموس خاص به بوصفه قيمة لكل مفتاح مقاطعة ➋، وسيحتاج كل من هذه القواميس بدوره إلى مفاتيح 'tracts' و 'pop' التي تبدأ بالقيمة الصحيحة 0. إذا شعرت بالضياع عند تتبّع بنية القاموس، فارجع إلى مثال القاموس في بداية هذه الفقرة. لن يفعل التابع setdefault() شيئًا إذا كان المفتاح موجودًا مسبقًا، وبالتالي يمكنك استدعاؤه في كل تكرار للحلقة for بدون مشاكل. الخطوة الثالثة: كتابة النتائج في ملف سيحتوي قاموس countyData بعد انتهاء حلقة for على جميع معلومات عدد السكان والمناطق المرتبطة بمفتاح المقاطعة county والولاية state، ويمكنك عندها برمجة مزيدٍ من الشيفرة البرمجية لكتابة هذه المعلومات في ملف نصي أو جدول بيانات إكسل آخر. لنستخدم الآن الدالة pprint.pformat() لكتابة قيمة قاموس countyData بوصفها سلسلة نصية ضخمة في ملف اسمه census2010.py، لذا أضِف الشيفرة البرمجية التالية إلى نهاية برنامجك، وتأكد من إبقائه بدون مسافة بادئة بحيث يبقى خارج حلقة for: #! python 3 # readCensusExcel.py - جدول عدد السكان وعدد المناطق الإحصائية لكل مقاطعة --snip-- for row in range(2, sheet.max_row + 1): --snip-- # افتح ملفًا نصيًا جديدًا واكتب محتويات بيانات المقاطعة countyData فيه print('Writing results...') resultFile = open('census2010.py', 'w') resultFile.write('allData = ' + pprint.pformat(countyData)) resultFile.close() print('Done.') ينتج عن الدالة pprint.pformat() سلسلةٌ نصية مُنسَّقة كشيفرة بايثون صالحة، والتي يمكنك إخراجها إلى ملف نصي اسمه census2010.py، وبالتالي سيتولد برنامج بايثون من برنامج بايثون الخاص بك. قد يبدو ذلك معقدًا، ولكن تتمثّل الفائدة في أنه يمكنك استيراد الملف census2010.py مثل أي وحدة بايثون أخرى. غيّر مجلد العمل الحالي إلى المجلد الذي يحتوي على الملف census2010.py ثم استورده في الصدفة التفاعلية كما يلي: >>> import os >>> import census2010 >>> census2010.allData['AK']['Anchorage'] {'pop': 291826, 'tracts': 55} >>> anchoragePop = census2010.allData['AK']['Anchorage']['pop'] >>> print('The 2010 population of Anchorage was ' + str(anchoragePop)) The 2010 population of Anchorage was 291826 يُعَد برنامج readCensusExcel.py عديم الجدوى، فلا حاجة لتشغيله مرة أخرى بعد حفظ نتائجه في الملف census2010.py، ويمكنك تشغيل الأمر import census2010 عندما تحتاج إلى بيانات المقاطعة. سيستغرق حساب هذه البيانات يدويًا ساعات يدويًا، ولكن أنجز هذا البرنامج هذا الأمر في بضع ثوان، ولن تواجه أيّ مشكلة في استخراج المعلومات المحفوظة في جدول بيانات إكسل وإجراء العمليات الحسابية عليها باستخدام وحدة OpenPyXL. ملاحظة: لا تنسَ أنه يمكنك تنزيل البرنامج الكامل. أفكار لبرامج مماثلة تستخدم العديد من الشركات والمكاتب برنامج إكسل لتخزين أنواع مختلفة من البيانات، وتصبح جداول البيانات كبيرة الحجم وغير عملية بسهولة. تمتلك البرامج التي تحلّل جدول بيانات إكسل بنية مماثلة، فهي تحمّل ملف جدول البيانات، وتجهّز بعض المتغيرات أو هياكل البيانات، ثم تتكرر على كل صف من الصفوف في جدول البيانات، حيث يمكن أن تفعل مثل هذه البرامج ما يلي: مقارنة البيانات في صفوف متعددة في جدول بيانات. فتح ملفات إكسل متعددة ومقارنة البيانات بين جداول بيانات. التحقق من احتواء جدول البيانات على صفوف فارغة أو بيانات غير صالحة في أيّ خلايا وتنبيه المستخدم في حالة وجود ذلك. قراءة البيانات من جدول البيانات واستخدامها كدخلٍ لبرامج بايثون الخاصة بك. الخلاصة تعرّفنا في هذا المقال على كيفية قراءة مستندات إكسل باستخدام بايثون، حيث وضّحنا كيفية فتح مستندات إكسل والحصول على الأوراق من المصنف والحصول على الخلايا والصفوف والأعمدة من الأوراق باستخدام وحدة OpenPyXL الخاصة بلغة بايثون، وطبّقنا هذه المعرفة على مثال عملي لقراءة البيانات من جدول بيانات يمثل الإحصاء السكان في الولايات المتحدة الأمريكية لعام 2010، وسنتابع في المقال التالي العمل على جداول بيانات إكسل من خلال توضيح كيفية الكتابة في مستندات إكسل باستخدام بايثون. ترجمة -وبتصرُّف- للقسم Reading Excel Documents من مقال Working with Excel Spreadsheets لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: استخراج البيانات من الويب عبر لغة بايثون Python قراءة وكتابة الملفات باستخدام لغة بايثون Python التعامل مع الملفات والمسارات في بايثون كيفية التعامل مع الملفات النصية في بايثون 3 نظرة عامة على برنامج مايكروسوفت إكسل Microsoft Excel النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
-
سنناقش في هذا المقال كيفية جداء الشعاع مععدد حقيقي، حيث تُسمَّى هذه العملية بتغيير الحجم Scaling، وكيفية استخدامها لبناء أشعة الوحدة Unit Vectors التي تُستخدَم لإظهار الاتجاه في الفضاء ثلاثي الأبعاد، وهي ضرورية للرسوميات الحاسوبية ثلاثية الأبعاد. سنوضح في هذا المقال المواضيع التالية: تغيير حجم الشعاع Scaling وخاصياته. معامل تغيير الحجم Scalar. مساواة العناصر المتقابلة في جبر الأشعة. أشعة الوحدة. توحيد Normalizing الأشعة. منحى Orientation شعاع الوحدة. المتغيرات في المعادلات الشعاعية. جداء الشعاع مع عدد حقيقي (تغيير حجم الشعاع Scaling) جرّب معرفة كيفية تعديل عناصر مصفوفة العمودية (3, 4)T بحيث يكون طول الشعاع الذي تمثله ضعف طوله الحالي ويبقى الشعاع يؤشّر إلى الاتجاه نفسه، إذ يمكنك التوصل إلى الإجابة الصحيحة بعناية باستخدام ما تعرفه مسبقًا، أو يمكنك التخمين ثم التحقق من صحة تخمينك كما يلي: 2 × (3, 4)T = (6, 😎T لنرى إن كان تخمينك صحيحًا كما يلي: (الطول الجديد)2 = 62 + 82 = 22 32 + 22 42 = 22 (32 + 42) = 22 × (الطول القديم)2. إذًا بأخذ الجذر للطرفين: (الطول الجديد) = 2 × (الطول القديم) ويكون الاتجاه نفسه باستخدام المثلثات المتشابهة، أو يمكنك ملاحظة أن: الاتجاه الجديد = arc tan( 8/6 ) = arc tan( 4/3 ) = الاتجاه القديم وبالتالي يبدو تخمينك صحيحًا. ملاحظة: يُطلَق على العدد الحقيقي أحيانًا اسم معامل تغيير الحجم (مقدار سلمي) Scalar عند الحديث عن الأشعة. تعني عملية تغيير حجم Scaling الشعاع الهندسي إبقاءَ منحاه Orientation كما هو مع تغيير طوله باستخدام معامل تغيير حجم، إذ تشبه هذه العملية تغيير حجم الصورة، حيث تتوسّع أو تتقلّص الكائنات مع بقاء اتجاهاتها كما كانت سابقًا. إذا كان الشعاع مُمثَّلًا بمصفوفة عمودية (x, y)T، فسيؤدي تغيير حجمه وفق عدد معين إلى جداء كل عنصر فيها بهذا العدد كما يلي: a(x, y)T = (ax, ay)T ينطبق ذلك أيضًا على الأشعة ثلاثية الأبعاد، فإذا مثّلنا الشعاع v بالمصفوفة العمودية (x, y, z)T، فسيكون تمثيل av بالمصفوفة العمودية (ax, ay, az)T. أوجد ناتج 0.5×(36.4, -18.9)T: 0.5×(36.4, -18.9)T = (0.5×36.4, 0.5× -18.9)T = ( 18.2, -9.45)T بعض خاصيات عملية تغيير حجم شعاع هناك العديد من خاصيات تغيير حجم الشعاع التي سنوضّحها فيما يلي. الشعاع الذي تغير حجمه هو شعاع أيضا إذا كان v شعاعًا، فإن: -1 v = -v 0 v = 0 لاحظ أنه يجب توخي الدقة في العبارتين السابقتين، حيث: -1 v = -v: عدد حقيقي * شعاع = شعاع 0 v = 0: عدد حقيقي * شعاع = شعاع نتيجة تغيير حجم شعاع هي شعاع أيضًا، ولاحظ أن 0 بالخط العريض هو شعاع تمثله المصفوفة العمودية (0,0,0)T، وليس العدد الحقيقي صفر. التعابير ذات الأنواع المختلطة أوجد ناتج 2 + 4( 3, 2, 5)T: لا يُعَد هذا التعبير صحيحًا، إذ لا يوجد معامل جمع + يطبّق العملية: عدد حقيقي + شعاع لذا من المفيد استخدام الأقواس لإظهار ما تعنيه بالضبط في التعابير التي تحتوي على أنواع ومعاملات مختلطة كما يلي: (2 + 1)(3, -5)T + 4(1, 2)T يمثّل المعامل + الأول عملية جمع لأعداد حقيقية (مقادير سلمية)، ويمثّل المعامل + الثاني عملية جمع لمصفوفات عمودية، لذا يجب تنفيذ العمليات كما يلي: (2 + 1)(3, -5)T + 4(1, 2)T = 3(3, -5)T + 4(1, 2)T = (3×3, 3× -5)T + (4×1, 4×2)T = (9, -15)T + (4, 😎T = (13, -7)T لا يُعَد تطبيق ذلك سيئًا كما يبدو، فما عليك إلّا تتبّع كل معامل وأنواع معامَلاته ونوع نتيجته. لنوجد الآن ناتج -1 (1, 2, -3)T - (2 + 1)(1, 0, 1)T: -1 (1, 2, -3)T - (2 + 1)(1, 0, 1)T = ( -1, -2, 3)T - 3(1, 0, 1)T = ( -1, -2, 3)T + (-3, 0, -3)T = (-4, -2, 0 )T جبر الأشعة إذا كان هناك عنصر مجهول أو اثنان في التعبير الذي يحتوي أنواع عمليات مختلطة، فيمكنك استخدام الجبر بالطريقة المعتادة للحل، ولكن كن حذرًا من أنّ كل خطوة تطبّقها تعطي تعبيرًا صالحًا. حل ما يلي مثلًا من أجل a و y: a(1, y)T + 2(0, 5 )T = (2, 20)T قد تقول أنه لا يمكن ذلك بسبب وجود مجهولين (a و y) مع معادلة واحدة فقط، ولكن: a(1, y)T + 2(0, 5)T = (2, 20)T (a, ay)T + (0, 10)T = (2, 20)T (a, ay+10)T = (2, 20)T يجب أن تكون العناصر المتقابلة متساوية، لذا: a = 2 ay + 10 = 20 2y = 10 y = 5 تُعَد هذه الخدعة شائعة وتسمى مساواة العناصر المتقابلة Equating Corresponding Elements. مساواة العناصر المتقابلة لنوجد قيمة x و y فيما يلي: 4( -1, y)T + 2( 3x, 10)T = (8, 24)T 4( -1, y)T + 2( 3x, 10)T = (8, 24)T ( -4, 4y)T + ( 6x, 20)T = (8, 24)T (6x-4, 4y+20)T = (8, 24)T 6x-4 = 8 4y+20 = 24 6x = 12 x = 2 4y = 4 y = 1 تعني الإشارةُ "=" في المعادلة الأصلية معادلةً شعاعية، بينما تؤدي مساواة العناصر المتقابلة إلى الحصول على معادلتين، حيث تعني الإشارةُ "=" معادلة عددية أو معادلة أعداد حقيقية. تتغير معادلة المصفوفات العمودية إلى عدة معادلات عددية باستخدام مساواة العناصر المتقابلة، حيث يكون لكل بُعد من المصفوفة العمودية معادلة واحدة، ويمكن بعد ذلك حل المعادلات العددية لإيجاد قيمة المجهول في بعض الأحيان وليس دائمًا. إليك مثلًا حالة لا تنجح فيها هذه الطريقة: أوجد قيمة x و y : 4(x, 5)T + 2(y, 1)T = 2(12, 11)T قد يبدو كل شيء جيدًا، ولكن يكشف تطبيق بعض الخطوات عن وجود مشكلة كما يلي: 4( x, 5)T + 2( y, 1)T = 2(12, 11)T (4x, 20)T + (2y, 2)T = (24, 22)T (4x+2y, 22 )T = (24, 22)T 4x + 2y = 24 22 = 22 لا تحتوي المعادلتان العدديتان اللتان تحصل عليهما من خلال مساواة العناصر المتقابلة على معلومات كافية للمتابعة. لنوجِد الآن قيمة a و x : a( -1, 5 )T + 2( 3x, 10 )T = ( 8, 25 )T يمكن أن نخمن أنه يمكننا إيجاد قيمة a و x كما يلي: a( -1, 5 )T + 2( 3x, 10 )T = ( 8, 25 )T ( -a, 5a)T + ( 6x, 20 )T = ( 8, 25 )T ( 6x-a, 5a+20 )T = ( 8, 25 )T ولنستخدم الآن مساواة العناصر المتقابلة: 6x-a = 8 5a+20 = 25 5a = 5 a = 1 6x-1 = 8 6x = 9 x = 9/6 x = 3/2 تغيير حجم شعاع باستخدام قيم أصغر من واحد نعلم من تعريف عملية تغيير حجم شعاع أنه: a( x, y )T = ( ax, ay )T فلا بأس إن كان a أصغر من واحد، فمثلًا: 0.5( x, y )T = ( 0.5x, 0.5y )T يمكن كتابة ذلك في بعض الأحيان على النحو التالي: شعاع الوحدة يبلغ طول الشعاع 1 لأنه محاذٍ للمحور x (في إطار الإحداثيات الحالي)، فالعنصر الوحيد غير الصفري هو 1.0 عند تمثيله بمصفوفة عمودية، ولكن يمكن أيضًا أن يساوي طول الأشعة غير المحاذية لأيّ محور واحدًا، وبالتالي فإن شعاع الوحدة Unit Vector هو شعاع طوله 1. هل يمثل (1, 1, 1)T شعاع وحدة؟ لا، لا يمثل شعاع وحدة لأن طوله لا يساوي 1: √( 12 + 12 + 12 ) = √3 = 1.7320508 إنشاء أشعة الوحدة يبلغ طول أشعة الوحدة 1، وإذا كان لديك شعاع معين v، فيمكنك استخدامه لإنشاء شعاع وحدة، وتسمى هذه العملية بتوحيد الأشعة Normalizing التي تُطبَّق كما يلي: احسب طول الشعاع v وهو | v |. غيّر حجم الشعاع v من خلال تقسيمه على طوله: .v / | v | تُكتَب هذه العملية في كثير من الأحيان بالصيغة التالية (يعني الحرف "u" المنخفض "شعاعَ وحدة Unit Vector"): vu = v / | v | إذا كان v = (x, y, z)T، فإن: vu = v / | v | = ( x /| v |, y /| v |, z /| v | )T تستخدم بعض المصادر ترميزًا مختلفًا لذلك، حيث يُستخدَم رمز "القبعة" لأشعة الوحدة î ولكنه لا يظهر جيدًا في المتصفحات، ويسمَّى شعاع الوحدة أحيانًا بالشعاع "المُوحَّد Normalized". جرب توحيد الشعاع (3, 4)T كما يلي: الشعاع هو الوتر في المثلث القائم 3-4-5، وبالتالي فإن طوله هو 5، وشعاع الوحدة هو: 1/5 × (3,4)T = (3/5, 4/5)T = (0.6, 0.8)T اتجاه شعاع الوحدة تؤدي عملية تغيير الحجم إلى تغيير طول الشعاع دون تغيير اتجاهه، حيث إذا كان vu هو شعاع الوحدة المقابل للشعاع v، فإن الشعاعين vu و v لهما المنحى نفسه. قد يبدو ذلك مفهومًا، ولكن لنوضّحه أكثر: ابدأ بالشعاع v = (3, 4)T. أنشئ شعاع الوحدة vu = (3/5, 4/5)T. اتجاه الشعاع v هو arc tan( 4/3 ). اتجاه vu هو: arc tan( (4/5) / (3/5) ) = arc tan( (4/5) × (5/3) ) = arc tan( 4/3 ) يمكننا إنشاء شعاع وحدة له اتجاه الشعاع w = (4, 6)T نفسه مثلًا كما يلي: | w | = √(16 + 36) = √52 wu = (4, 6)T / ( √52) ولنتأكد الآن من صحة الإجابة: | wu |T = (4T + 6T) / ( √52)T = (16 + 36)/52 = 52/52 =1.0 | wu | = 1.0 يؤشّر شعاع الوحدة المتشكّل من شعاع ثلاثي الأبعاد إلى اتجاه الشعاع الأصلي نفسه أيضًا، إذًا لنجرّب مثلًا توحيد الشعاع g = (-3, 4, -1)T: | g | = √(9 + 16 + 1 ) = √26 gu = (-3, 4, -1)T / (√26) الاتجاه المعاكس يمثل (1, 1)T / (√2) شعاعَ وحدة له منحى مقداره 45 درجة، وطوله √( 12/2 + 12/2 ) = 1، وزاويته مقدارها arctan(1/1) = 45 درجة، ولكن يمكنك إنشاء شعاع وحدة يؤشّر في الاتجاه المعاكس لهذا الشعاع من خلال أخذ معاكس كل مكوّن من مكوناته كما يلي: -( 1, 1)T / (√2) = (-1, -1)T / (√2) تدريب: أنشئ شعاع وحدة يؤشّر إلى الاتجاه المعاكس للشعاع (3, 0, 2)T: طول الشعاع = √(9+4) = √13 شعاع الوحدة في الاتجاه الأول = (3, 0, 2)T / ( √13) شعاع الوحدة في الاتجاه المعاكس = ( -3, 0, -2)T / ( √13) بهذا نكون قد وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على مفهوم تغيير حجم الأشعة وشعاع الوحدة، وسنناقش في المقال التالي الجداء النقطي Dot Product لشعاعين في التصاميم ثلاثية الأبعاد. ترجمة -وبتصرُّف- للفصل Scaling and Unit Vectors من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: خاصية الاتجاه Direction للأشعة الهندسية في التصاميم ثلاثية الأبعاد الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد التعرف على النقاط والخطوط في الرسوميات الحاسوبية ثلاثية الأبعاد كيفية جمع الأشعة الهندسية للتصاميم ثلاثية الأبعاد
-
تعرّفتَ في المقالات السابقة على معلومات كافية لكتابة برامج أكثر تعقيدًا، ولكنك ستعثر على أخطاء كثيرة فيها، لذا سنوضّح في هذا المقال بعض الأدوات والتقنيات لإيجاد السبب الجذري للأخطاء في برنامجك لمساعدتك في إصلاح الأخطاء بسرعةٍ أكبر وبجهد أقل. يمكن القول أن كتابة الشيفرة البرمجية تمثّل 90% من عملية البرمجة، ولكن يمثّل تنقيح أخطاء هذه الشيفرة البرمجية نسبة 90% أخرى من العمل، حيث سينفّذ حاسوبك ما تطلب منه فقط، إذ لن يقرأ أفكارك أو يطبّق ما تنوي فعله. يتسبّب المبرمجون وحتى المحترفون منهم بالأخطاء طوال الوقت، لذلك لا تشعر بالإحباط إذا واجه برنامجك مشكلةً ما. يوجد عددٌ من الأدوات والتقنيات لتحديد ما تفعله شيفرتك البرمجية بالضبط ومكان حدوث الخطأ لحسن الحظ، حيث سنوضّح في هذا المقال أولًا التسجيل Logging والتأكيدات Assertions، وهما ميزتان مساعدتان في اكتشاف الأخطاء في وقت مبكر، إذ يصبح إصلاح الأخطاء أسهل كلما اكتشفتَها مبكرًا. سنوضّح بعد ذلك كيفية استخدام منقّح الأخطاء Debugger الذي يمثّل ميزةً من ميزات المحرّر Mu، حيث ينفّذ منقّح الأخطاء البرنامج من خلال تنفيذ تعليمةٍ واحدة في كل مرة، مما يمنحك فرصة لفحص القيم في المتغيرات أثناء تشغيل شيفرتك البرمجية وتعقّب كيفية تغير هذه القيم عبر برنامجك بأكمله. يُعَد ذلك أبطأ بكثير من تشغيل البرنامج بأقصى سرعة، ولكنه مفيد لرؤية القيم الفعلية في البرنامج أثناء تشغيله بدلًا من استنتاج ما ستكون عليه القيم من الشيفرة المصدرية. رفع الاستثناءات Raising Exceptions ترفع لغة بايثون Python استثناءً عندما تحاول تنفيذ شيفرة برمجية غير صالحة، حيث تعرّفنا في مقالٍ سابق على كيفية التعامل مع استثناءات بايثون باستخدام تعليمتي try و except حتى يتمكّن برنامجك من التعافي من الاستثناءات المُتوقَّعة. يمكنك أيضًا رفع استثناءاتك الخاصة في شيفرتك البرمجية، حيث يمثّل رفع الاستثناء طريقةً لإيقاف تشغيل الشيفرة البرمجية في هذه الدالة ونقل تنفيذ البرنامج إلى التعليمة except. تُرفَع الاستثناءات باستخدام التعليمة raise التي تتكون ممّا يلي: الكلمة المفتاحية raise. استدعاء الدالة Exception(). سلسلة نصية تحتوي على رسالة خطأ مفيدة نمرَّرها إلى الدالة Exception(). لندخِل مثلًا ما يلي في الصدفة التفاعلية Interactive Shell: >>> raise Exception('This is the error message.') Traceback (most recent call last): File "<pyshell#191>", line 1, in <module> raise Exception('This is the error message.') Exception: This is the error message. إن لم تُوجَد تعليمات try و except المغلِّفة للتعليمة except التي ترفع الاستثناء، فسيتعطل البرنامج ويعرض رسالة خطأ الاستثناء ببساطة. تعرِف الشيفرة البرمجية التي تستدعي الدالة -وليس الدالة نفسها- كيفية التعامل مع الاستثناء، وهذا يعني أنك سترى التعليمة raise ضمن الدالة وتعليمتي try و except في الشيفرة البرمجية التي تستدعي هذه الدالة. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد، وأدخِل مثلًا الشيفرة البرمجية التالية واحفظ البرنامج بالاسم boxPrint.py: def boxPrint(symbol, width, height): if len(symbol) != 1: ➊ raise Exception('Symbol must be a single character string.') if width <= 2: ➋ raise Exception('Width must be greater than 2.') if height <= 2: ➌ raise Exception('Height must be greater than 2.') print(symbol * width) for i in range(height - 2): print(symbol + (' ' * (width - 2)) + symbol) print(symbol * width) for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)): try: boxPrint(sym, w, h) ➍ except Exception as err: ➎ print('An exception happened: ' + str(err)) اطّلع على تنفيذ هذا البرنامج، حيث عرّفنا الدالة boxPrint() التي تأخذ محرفًا وعرضًا وارتفاعًا، وتستخدم هذا المحرف لإنشاء صورة صغيرة لمربع له هذا العرض والارتفاع، وتطبع شكل هذا المربع على الشاشة. لنفترض أننا نريد أن يكون هذا المحرف مفردًا، وأن يكون العرض والارتفاع أكبر من 2، فسنضيف تعليمات if لرفع الاستثناءات عند عدم استيفاء هذه المتطلبات. ستتعامل لاحقًا تعليمات try/except مع الوسطاء غير الصالحة عندما نستدعي الدالة boxPrint() مع وسطاء مختلفة. يستخدم هذا البرنامج الصيغة except Exception as err للتعليمة except ➍. إذا أُعيد الكائن Exception من الدالة boxPrint() ➊ ➋ ➌، فستخزِّنه التعليمة except في متغير بالاسم err، ثم يمكننا تحويل الكائن Exception إلى سلسلة نصية من خلال تمريره إلى الدالة str() لإعطاء رسالة خطأ مألوفة للمستخدم ➎، وسيبدو الخرج كما يلي عند تشغيل البرنامج boxPrint.py: **** * * * * **** OOOOOOOOOOOOOOOOOOOO O O O O O O OOOOOOOOOOOOOOOOOOOO An exception happened: Width must be greater than 2. An exception happened: Symbol must be a single character string. يمكنك التعامل مع الأخطاء بأمان أكبر باستخدام تعليمات try و except بدلًا من ترك البرنامج يتعطل بأكمله. الحصول على التعقب العكسي Traceback كسلسلة نصية إذا واجهت شيفرة بايثون خطأً ما، فستعطي كنزًا من المعلومات عن الخطأ، حيث يُسمّى هذه المعلومات بالتعقّب العكسي Traceback الذي يتضمّن رسالة الخطأ ورقم السطر الذي تسبّب في الخطأ وسلسلة استدعاءات الدوال التي أدّت إلى الخطأ، وتسمّى هذه السلسلة من الاستدعاءات بمكدس الاستدعاءات Call Stack. افتح تبويبًا جديدًا في محرّر Mu لإنشاء ملف جديد، وأدخِل البرنامج التالي واحفظه بالاسم errorExample.py: def spam(): beef() def beef(): raise Exception('This is the error message.') spam() سيبدو الخرج كما يلي عند تشغيل البرنامج errorExample.py: Traceback (most recent call last): File "errorExample.py", line 7, in <module> spam() File "errorExample.py", line 2, in spam beef() File "errorExample.py", line 5, in beef raise Exception('This is the error message.') Exception: This is the error message. يمكنك أن ترى من التعقّب العكسي السابق أن الخطأ حدث في السطر رقم 5 في الدالة beef()، وأتى هذا الاستدعاء للدالة beef() من السطر رقم 2 في الدالة spam() التي اُستدعيت بدورها في السطر رقم 7. يمكن أن يساعدك مكدس الاستدعاءات في تحديد الاستدعاء الذي أدى إلى الخطأ في البرامج التي يمكن فيها استدعاء الدوال من أماكن متعددة. تعرض لغة بايثون التعقّب العكسي عند عدم التعامل مع الاستثناء المرفوع، ولكن يمكنك أيضًا الحصول على التعقب العكسي بوصفه سلسلة نصية من خلال استدعاء الدالة traceback.format_exc()، حيث تكون هذه الدالة مفيدة إذا أردتَ الحصول على المعلومات من التعقب العكسي الخاص بالاستثناء ولكنك تريد أيضًا التعليمة except للتعامل مع الاستثناء بأمان. يجب استيراد الوحدة traceback الخاصة بلغة بايثون قبل استدعاء هذه الدالة. يمكنك مثلًا كتابة معلومات التعقّب العكسي في ملف نصي والحفاظ على تشغيل البرنامج بدلًا من تعطّل برنامجك عند حدوث الاستثناء، حيث يمكنك إلقاء نظرة على الملف النصي لاحقًا عندما تكون مستعدًا لتنقيح أخطاء برنامجك. أدخِل الآن ما يلي في الصدفة التفاعلية: >>> import traceback >>> try: ... raise Exception('This is the error message.') except: ... errorFile = open('errorInfo.txt', 'w') ... errorFile.write(traceback.format_exc()) ... errorFile.close() ... print('The traceback info was written to errorInfo.txt.') 111 The traceback info was written to errorInfo.txt. تُعَد القيمة 111 هي القيمة التي يعيدها التابع write()، حيث كُتِبت المحارف 111 في الملف، وكُتِب نص التعقّب العكسي في الملف errorInfo.txt: Traceback (most recent call last): File "<pyshell#28>", line 2, in <module> Exception: This is the error message. سنتعلّم لاحقًا كيفية استخدام الوحدة logging التي تُعَد أكثر فعالية من مجرد كتابة معلومات الخطأ في ملفات نصية. التأكيدات Assertions يُعَد التأكيد Assertion فحص سلامةٍ للتأكد من أن شيفرتك البرمجية لا تفعل شيئًا خاطئًا، حيث يمكن إجراء عمليات التحقق من السلامة من خلال استخدام التعليمة assert، وإذا فشل التحقق من السلامة، فسيُرفَع الاستثناء AssertionError. تتكون التعليمة assert مما يلي في الشيفرة البرمجية: الكلمة المفتاحية assert. شرط (أيّ تعبير يمكن تقييمه بالقيمة True أو False). فاصلة. سلسلة نصية تُعرَض عندما تكون قيمة الشرط False. تمثّل التعليمة assert التأكيد على أن الشرط صحيح، وإذا لم يكن الأمر كذلك، فلا بد من وجود خطأٍ في مكانٍ ما، لذا يجب إيقاف البرنامج مباشرةً. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73] >>> ages.sort() >>> ages [15, 17, 22, 26, 47, 54, 57, 73, 80, 92] >>> assert ages[0] <= ages[-1] # تأكيد أن العمر الأول <= العمر الأخير تؤكّد التعليمة assert في المثال السابق على أن العنصر الأول في القائمة ages يجب أن يكون أصغر من العنصر الأخير أو يساويه، ويمثّل ذلك التحقق من السلامة، حيث إذا كانت الشيفرة البرمجية الموجودة في التابع sort() خالية من الأخطاء وأدت عملها، فسيكون التأكيد صحيحًا. يُقيَّم التعبير ages[0] <= ages[-1] على أنه True، وبالتالي لن تفعل التعليمة assert شيئًا، ولكن لنتظاهر بوجود خطأ في شيفرتنا البرمجية، ولنفترض أننا استدعينا عن طريق الخطأ تابعَ القائمة reverse() بدلًا من تابع القائمة sort()، حيث سترفع التعليمة assert خطأ AssertionError مثلًا عندما ندخِل ما يلي في الصدفة التفاعلية: >>> ages = [26, 57, 92, 54, 22, 15, 17, 80, 47, 73] >>> ages.reverse() >>> ages [73, 47, 80, 17, 15, 22, 54, 92, 57, 26] >>> assert ages[0] <= ages[-1] # تأكيد أن العمر الأول <= العمر الأخير Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError يجب ألّا تتعامل شيفرتك البرمجية مع التعليمة assert باستخدام تعليمات try و except على عكس الاستثناءات، حيث إذا فشلت التعليمة assert، فيُفترَض أن يتعطل برنامجك، وبالتالي ستختصر الوقت المنقضي من حدوث السبب الأصلي للخطأ إلى وقت ملاحظة الخطأ لأول مرة من خلال هذا الفشل السريع، مما سيؤدي إلى تقليل الشيفرة البرمجية التي يجب أن تتحقق منها قبل العثور على سبب الخطأ. تُعَد التأكيدات مُخصَّصة لأخطاء المبرمج وليست خاصة بأخطاء المستخدم، إذ يجب أن تفشل التأكيدات فقط عندما يكون البرنامج قيد التطوير، ويجب ألّا يرى المستخدم أبدًا خطأ تأكيد في البرنامج النهائي، لذا يجب أن ترفع استثناءً بدلًا من اكتشافه باستخدام التعليمة assert بالنسبة للأخطاء التي يمكن أن يتعرض لها برنامجك كجزءٍ عادي من عمله مثل عدم العثور على ملف أو إدخال المستخدم بيانات غير صالحة، ويجب ألّا تستخدم تعليمات assert بدلًا من رفع الاستثناءات، لأنه يمكن للمستخدمين اختيار إيقاف التأكيدات. إذا شغّلتَ سكربت بايثون باستخدام الأمر python -O myscript.py بدلًا من الأمر python myscript.py، فستتخطّى شيفرة بايثون تعليمات assert، وقد يعطّل المستخدمون التأكيدات عندما يطورون برنامجًا ويحتاجون إلى تشغيله في بيئة الإنتاج التي تتطلب أداءً أعلى، بالرغم من أنهم في كثير من الحالات سيتركون التأكيدات مفعّلة حتى في ذلك الوقت. لا تُعَد التأكيدات بديلًا عن الاختبار الشامل، فمثلًا إذا ضبطنا القائمة ages في المثال السابق على القيمة [10, 3, 2, 1, 20]، فلن تلاحظ تعليمة التأكيد assert ages[0] <= ages[-1] أن القائمة غير مرتبة، لأنها ترى أن العمر الأول أصغر من أو يساوي العمر الأخير، وهو الشيء الوحيد الذي تتحقق منه تعليمة التأكيد. استخدام التأكيد في برنامج لمحاكاة إشارات المرور لنفترض أنك تنشئ برنامجًا لمحاكاة إشارات المرور، حيث يكون هيكل البيانات الذي يمثّل إشارات التوقف عند التقاطع هو قاموس له المفاتيح 'ns' و 'ew' لإشارات التوقف التي تمثّل جهة الشمال-الجنوب وجهة الشرق-الغرب على التوالي. ستكون القيم الموجودة في هذه المفاتيح إحدى السلاسل النصية 'green' أو 'yellow' أو 'red'، حيث ستبدو الشيفرة البرمجية كما يلي: market_2nd = {'ns': 'green', 'ew': 'red'} mission_16th = {'ns': 'red', 'ew': 'green'} يمثّل المتغيران السابقان تقاطعات شارع السوق Market Street والشارع الثاني 2nd Street، وشارع ميشن Mission Street والشارع السادس عشر 16th Street. نبدأ المشروع من خلال كتابة الدالة switchLights() التي تأخذ قاموسًا يمثّل التقاطع بوصفه وسيطًا وتبّدل بين الأضواء. نعتقد في البداية أن الدالة switchLights() يجب أن تحوّل ببساطة كل ضوء إلى اللون التالي في السلسلة، حيث يجب أن تتغيّر جميع القيم 'green' إلى القيمة 'yellow'، ويجب أن تتغير قيم 'yellow' إلى القيم 'red'، ويجب أن تتغير القيم 'red' إلى القيم 'green'، إذ قد تبدو الشيفرة البرمجية لتطبيق هذه الفكرة كما يلي: def switchLights(stoplight): for key in stoplight.keys(): if stoplight[key] == 'green': stoplight[key] = 'yellow' elif stoplight[key] == 'yellow': stoplight[key] = 'red' elif stoplight[key] == 'red': stoplight[key] = 'green' switchLights(market_2nd) لا بد أنك رأيتَ مشكلة هذه الشيفرة البرمجية، ولكن لنفترض أنك كتبتَ بقية شيفرة المحاكاة التي يبلغ طولها آلاف الأسطر دون أن تلاحظ ذلك، حيث لن يتعطل البرنامج عندما تشغّل المحاكاة في النهاية، ولكن ستتعطّل سياراتك الافتراضية في البرنامج. لن يكون لديك أيّ فكرة عن مكان وجود الخطأ بما أنك كتبتَ بقية البرنامج فعليًا، إذ قد يكون الخطأ في الشيفرة البرمجية التي تحاكي السيارات أو في الشيفرة البرمجية التي تحاكي السائقين الافتراضيين، وبالتالي قد يستغرق الأمر ساعات لتعقّب الخطأ العكسي إلى الدالة switchLights(). إذا أضفتَ تأكيدًا أثناء كتابة الدالة switchLights() للتحقّق من أن أحد الأضواء يكون دائمًا باللون الأحمر على الأقل، فيمكن تضمين ما يلي في نهاية الدالة: assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight) سيتعطّل برنامجك مع ظهور رسالة الخطأ التالية باستخدام التأكيد السابق: Traceback (most recent call last): File "carSim.py", line 14, in <module> switchLights(market_2nd) File "carSim.py", line 13, in switchLights assert 'red' in stoplight.values(), 'Neither light is red! ' + str(stoplight) ➊ AssertionError: Neither light is red! {'ns': 'yellow', 'ew': 'green'} السطر المهم في المثال السابق هو AssertionError ➊، حيث لا يُعَد تعطّل برنامجك أمرًا مثاليًا، ولكنه يشير مباشرةً إلى فشل التحقق من السلامة، إذ لا يحتوي أي من اتجاهي حركة المرور على ضوء أحمر، مما يعني أن حركة المرور يمكن أن تسير في كلا الاتجاهين من التقاطع. يمكنك توفير الكثير من جهد تنقيح الأخطاء مستقبلًا من خلال الفشل السريع في وقتٍ مبكر من تنفيذ البرنامج. التسجيل Logging إذا سبق لك أن وضعتَ التعليمة print() في شيفرتك البرمجية لإنتاج قيمة بعض المتغيرات أثناء تشغيل البرنامج، فلا بد أنك استخدمتَ صيغة تسجيلٍ لتنقيح أخطاء شيفرتك البرمجية، حيث يُعَد التسجيل طريقةً رائعة لفهم ما يحدث في برنامجك وترتيب حدوثه. تسهّل الوحدة logging في بايثون إنشاء سجلٍ للرسائل المُخصَّصة التي تكتبها، إذ توضّح هذه الرسائل وقت وصول تنفيذ البرنامج إلى استدعاء دالة التسجيل وتسرد المتغيرات التي حدّدتها في ذلك الوقت. بينما تشير رسالة السجل الناقصة إلى تخطي جزء من الشيفرة البرمجية وعدم تنفيذه مطلقًا. استخدام الوحدة logging يمكن تفعيل الوحدة logging لعرض رسائل السجل على شاشتك أثناء تشغيل البرنامج من خلال نسخ ما يلي إلى بداية برنامجك، ولكن ضمّن السطر Shebang الذي هو #! python: import logging logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname) s - %(message)s') لا داعي للقلق كثيرًا بشأن كيفية عمل السطر السابق، ولكنه ينشئ الكائن LogRecord الذي يحتوي على معلومات حول حدثٍ ما عندما تسجّل بايثون هذا الحدث. تتيح لك الدالة basicConfig() الخاصة بالوحدة logging تحديدَ التفاصيل المتعلقة بكائن LogRecord الذي تريد رؤيته وكيفية عرض هذه التفاصيل. لنفترض أنك كتبتَ دالةً لحساب عاملي Factorial عددٍ ما، حيث يكون عاملي العدد 4 في الرياضيات هو 1 × 2 × 3 × 4 أو القيمة 24 وعاملي العدد 7 هو 1 × 2 × 3 × 4 × 5 × 6 × 7 أو القيمة 5040. افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد وأدخِل الشيفرة البرمجية التالية التي تحتوي على خطأ، ولكنك ستدخِل أيضًا عددًا من رسائل السجل لمساعدة نفسك في اكتشاف الخطأ الذي يحدث، واحفظ البرنامج بالاسم factorialLog.py: import logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') logging.debug('Start of program') def factorial(n): logging.debug('Start of factorial(%s%%)' % (n)) total = 1 for i in range(n + 1): total *= i logging.debug('i is ' + str(i) + ', total is ' + str(total)) logging.debug('End of factorial(%s%%)' % (n)) return total print(factorial(5)) logging.debug('End of program') نستخدم الدالة logging.debug() عندما نريد طباعة معلومات السجل، وتستدعي الدالةُ debug() الدالةَ basicConfig() وستطبع سطرًا من المعلومات، حيث ستكون هذه المعلومات بالتنسيق الذي حددناه في الدالة basicConfig() وستتضمّن الرسائل التي مرّرناها إلى الدالة debug(). يُعَد استدعاء الدالة print(factorial(5)) جزءًا من البرنامج الأصلي، لذا ستُعرَض النتيجة حتى لو تعطّلت رسائل التسجيل. يبدو خرج هذا البرنامج كما يلي: 2024-05-23 16:20:12,664 - DEBUG - Start of program 2024-05-23 16:20:12,664 - DEBUG - Start of factorial(5) 2024-05-23 16:20:12,665 - DEBUG - i is 0, total is 0 2024-05-23 16:20:12,668 - DEBUG - i is 1, total is 0 2024-05-23 16:20:12,670 - DEBUG - i is 2, total is 0 2024-05-23 16:20:12,673 - DEBUG - i is 3, total is 0 2024-05-23 16:20:12,675 - DEBUG - i is 4, total is 0 2024-05-23 16:20:12,678 - DEBUG - i is 5, total is 0 2024-05-23 16:20:12,680 - DEBUG - End of factorial(5) 0 2024-05-23 16:20:12,684 - DEBUG - End of program تعيد الدالة factorial() القيمة 0 بوصفها ناتج عاملي العدد 5، وهذا ليس صحيحًا، حيث يجب أن تضرب حلقة for القيمة الموجودة في المتغير total بالأعداد من 1 إلى 5، ولكن توضّح رسائل السجل التي تعرضها الدالة logging.debug() أن المتغير i يبدأ من القيمة 0 بدلًا من القيمة 1، وبالتالي تكون قيمة بقية التكرارات خاطئة للمتغير total أيضًا، لأن ضرب الصفر بأيّ شيء يساوي صفرًا. توفّر رسائل التسجيل سلسلةً من مسارات التنقل التي يمكن أن تساعدك في معرفة متى بدأت الأمور تسوء. غيّر السطر for i in range(n + 1): إلى for i in range(1, n + 1):، وشغّل البرنامج مرة أخرى، وسيبدو الخرج كما يلي: 2024-05-23 17:13:40,650 - DEBUG - Start of program 2024-05-23 17:13:40,651 - DEBUG - Start of factorial(5) 2024-05-23 17:13:40,651 - DEBUG - i is 1, total is 1 2024-05-23 17:13:40,654 - DEBUG - i is 2, total is 2 2024-05-23 17:13:40,656 - DEBUG - i is 3, total is 6 2024-05-23 17:13:40,659 - DEBUG - i is 4, total is 24 2024-05-23 17:13:40,661 - DEBUG - i is 5, total is 120 2024-05-23 17:13:40,661 - DEBUG - End of factorial(5) 120 2024-05-23 17:13:40,666 - DEBUG - End of program يؤدي استدعاء الدالة factorial(5) إلى إعادة القيمة 120 الصحيحة، وتظهِر رسائل السجل ما يحدث ضمن الحلقة، مما يقودك إلى الخطأ مباشرةً. يمكنك أن ترى أن استدعاءات الدالة logging.debug() لا تطبع السلاسل النصية المُمرَّرة إليها فقط، بل تطبع أيضًا العلامة الزمنية Timestamp والكلمة DEBUG. لا تنقح الأخطاء باستخدام الدالة print() تُعَد كتابة التعليمتين import logging و logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') أمرًا صعبًا بعض الشيء، لذا سترغب في استخدام استدعاءات الدالة print() بدلًا من ذلك، ولكن لا تنخدع بسهولة استخدام هذه الدالة، لأنك ستقضي كثيرًا من الوقت في إزالة استدعاءات الدالة print() من شيفرتك البرمجية لجميع رسائل السجل بعد الانتهاء من تنقيح الأخطاء، وقد تزيل أيضًا عن طريق الخطأ بعض استدعاءات الدالة print() المُستخدَمة للرسائل التي ليس لها علاقة بالسجل. يمكنك ملء برنامجك بالعدد الذي تريده من رسائل السجل، ويمكنك دائمًا تعطيلها لاحقًا من خلال إضافة استدعاء واحد للدالة logging.disable(logging.CRITICAL)، حيث تسهّل الوحدة logging التبديل بين إظهار وإخفاء رسائل السجل على عكس الدالة print(). تُعَد رسائل السجل خاصةً بالمبرمج وليست خاصة بالمستخدم، إذ لن يهتم المستخدم بمحتويات بعض قيم القاموس التي تحتاج إلى رؤيتها للمساعدة في تنقيح الأخطاء، لذا استخدم رسالة سجل لذلك، ولكن يجب أن تستخدم استدعاء الدالة print() بالنسبة للرسائل التي يرغب المستخدم في رؤيتها مثل "عدم العثور على ملف File not found" أو "قيمة إدخال غير صالحة لذا أدخِل قيمة عددية من فضلك Invalid input, please enter a number"، فلن ترغب في حرمان المستخدم من المعلومات المفيدة له بعد تعطيل رسائل السجل. مستويات التسجيل توفر مستويات التسجيل طريقة لتصنيف رسائل السجل حسب الأهمية، إذ توجد خمسة مستويات للتسجيل في لغة بايثون التي سنوضحها في الجدول التالي من الأقل إلى الأكثر أهمية، حيث يمكن تسجيل الرسائل على كل مستوى باستخدام دالة تسجيل مختلفة: مستوى التسجيل دالة التسجيل وصفها المستوى DEBUG الدالة logging.debug() المستوى الأدنى، ويُستخدَم للتفاصيل الصغيرة، حيث تهتم بهذه الرسائل عند تشخيص المشاكل فقط. المستوى INFO الدالة logging.info() يُستخدَم لتسجيل معلومات عن الأحداث العامة في برنامجك أو للتأكد من أن الأمور تسير جيدًا في البرنامج. المستوى WARNING الدالة logging.warning() يُستخدم للإشارة إلى مشكلة محتملة لا تمنع البرنامج من العمل ولكنها قد تفعل ذلك مستقبلًا. المستوى ERROR الدالة logging.error() يُستخدم لتسجيل خطأٍ تسبّب في فشل البرنامج بعمل شيءٍ ما. المستوى CRITICAL الدالة logging.critical() المستوى الأعلى، ويُستخدَم للإشارة إلى خطأ كبير تسبّب أو أنه على وشك التسبّب في توقف البرنامج عن العمل بالكامل. تُمرَّر رسالة التسجيل بوصفها سلسلة نصية إلى هذه الدوال. تُعَد مستويات التسجيل مجرد اقتراحات، فالأمر متروك لك لتحديد الفئة التي تندرج ضمنها رسالة السجل الخاصة بك. لندخِل الآن ما يلي في الصدفة التفاعلية: >>> import logging >>> logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s') >>> logging.debug('Some debugging details.') 2024-05-18 19:04:26,901 - DEBUG - Some debugging details. >>> logging.info('The logging module is working.') 2024-05-18 19:04:35,569 - INFO - The logging module is working. >>> logging.warning('An error message is about to be logged.') 2024-05-18 19:04:56,843 - WARNING - An error message is about to be logged. >>> logging.error('An error has occurred.') 2024-05-18 19:05:07,737 - ERROR - An error has occurred. >>> logging.critical('The program is unable to recover!') 2024-05-18 19:05:45,794 - CRITICAL - The program is unable to recover! تتمثّل فائدة مستويات التسجيل في أنه يمكنك تغيير أولوية رسالة التسجيل التي تريد رؤيتها، حيث سيؤدي تمرير المستوى logging.DEBUG إلى وسيط الكلمة المفتاحية Keyword Argument الذي هو level الخاص بالدالة basicConfig() إلى إظهار الرسائل من جميع مستويات التسجيل، حيث يُعَد المستوى DEBUG هو المستوى الأدنى. قد تكون مهتمًا بالأخطاء فقط بعد تطوير برنامجك، وبالتالي يمكنك ضبط الوسيط level الخاص بالدالة basicConfig() على المستوى logging.ERROR في هذه الحالة، إذ سيعرض هذا المستوى رسائل المستوى ERROR ورسائل المستوى CRITICAL فقط ويتخطى رسائل المستويات DEBUG و INFO و WARNING. تعطيل التسجيل لا بد أنك تفضّل عدم إظهار جميع رسائل السجل التي تؤدي إلى جعل الشاشة مزدحمة بعد تنقيح أخطاء برنامجك، لذا توجد الدالة logging.disable() التي تعمل على تعطيل رسائل السجل حتى لا تضطر إلى الدخول إلى برنامجك وإزالة جميع استدعاءات التسجيل يدويًا. يمكنك تمرير هذه الدالة إلى مستوى التسجيل فقط، وستمنع هذه الدالة جميع رسائل السجل عند هذا المستوى أو المستويات الأقل، لذا إذا أردتَ تعطيل التسجيل بالكامل، فما عليك سوى إضافة الاستدعاء logging.disable(logging.CRITICAL) إلى برنامجك. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import logging >>> logging.basicConfig(level=logging.INFO, format=' %(asctime)s - %(levelname)s - %(message)s') >>> logging.critical('Critical error! Critical error!') 2024-05-22 11:10:48,054 - CRITICAL - Critical error! Critical error! >>> logging.disable(logging.CRITICAL) >>> logging.critical('Critical error! Critical error!') >>> logging.error('Error! Error!') ستعطّل الدالة logging.disable() جميع الرسائل بعدها، لذا يُحتمَل أنك تريد إضافتها بالقرب من سطر الاستيراد import logging في برنامجك، وبالتالي يمكنك بسهولة العثور عليه لتعليق هذا الاستدعاء أو إلغاء تعليقه لتفعيل رسائل التسجيل أو تعطيلها حسب الحاجة. التسجيل في ملف يمكنك كتابة رسائل السجل في ملف نصي بدلًا من عرضها على الشاشة، حيث تأخذ الدالة logging.basicConfig() وسيط الكلمة المفتاحية filename كما يلي: import logging logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s') ستُحفَظ رسائل السجل في الملف myProgramLog.txt.يمكن أن تؤدي رسائل التسجيل إلى جعل شاشتك مزدحمة بالعناصر وتجعل من الصعب قراءة خرج البرنامج بالرغم من فوائدها التي تحدّثنا عنها سابقًا، لذا كتبنا رسائل التسجيل في ملف لإبقاء شاشتك واضحة ولتخزين الرسائل حتى تتمكّن من قراءتها بعد تشغيل البرنامج، حيث يمكنك فتح هذا الملف النصي في أي محرّر نصوص مثل المفكرة Notepad أو TextEdit. منقح أخطاء المحرر Mu يُعَد منقّح الأخطاء من ميزات المحرّر Mu و IDLE وبرامج تحرير النصوص الأخرى التي تسمح لك بتنفيذ برنامجك من خلال تنفيذ سطر واحد في كل مرة، حيث يشغّل منقّح الأخطاء سطرًا واحد من الشيفرة البرمجية ثم ينتظر حتى تخبره بالمتابعة. يمكنك أن تأخذ الوقت الذي تريده لفحص القيم الموجودة في المتغيرات في أي مرحلة معينة من عمر البرنامج من خلال تشغيل برنامجك مع منقّح الأخطاء باستخدام هذه الطريقة، إذ يُعَد منقّح الأخطاء أداةً مفيدة لتعقّب الأخطاء. يمكنك تشغيل برنامج مع منقّح أخطاء المحرّر Mu من خلال النقر على زر تنقيح الأخطاء "Debug" في الصف العلوي من الأزرار بجانب زر التشغيل "Run". ستفتح نافذة فحص تنقيح الأخطاء "Debug Inspector" على طول الجانب الأيمن من النافذة بالإضافة إلى نافذة الخرج المعتاد في الأسفل، حيث تسرد نافذة فحص تنقيح الأخطاء قيم المتغيرات الحالية في برنامجك. يوقِف منقّح الأخطاء في الشكل التالي تنفيذ البرنامج قبل تشغيل السطر الأول من الشيفرة البرمجية، حيث يمكنك رؤية هذا السطر مميزًا في محرّر الملفات: تشغيل المحرّر Mu لبرنامجٍ ما مع منقّح الأخطاء يضيف وضع تنقيح الأخطاء أيضًا أزرارًا جديدة إلى أعلى المحرّر وهي: زر المتابعة "Continue" وزر "Step Over" وزر "Step In" وزر "Step Out"، ويوجد زر التوقف "Stop" المعتاد أيضًا. زر المتابعة Continue يؤدي النقر على زر المتابعة "Continue" إلى تنفيذ البرنامج بطريقة طبيعية حتى ينتهي البرنامج أو يصل إلى نقطة توقف Breakpoint (سنوضح نقاط التوقف لاحقًا في هذا المقال). إذا انتهيتَ من تنقيح الأخطاء وأردتَ أن يتابع البرنامج عمله بطريقة طبيعية، فانقر على زر المتابعة "Continue". زر Step In يؤدي النقر على زر "Step In" إلى أن ينفّذ منقّح الأخطاء السطر التالي من الشيفرة البرمجية ثم يوقفه مرةً أخرى. إذا كان السطر التالي من الشيفرة البرمجية هو استدعاء دالة، فسيدخل منقّح الأخطاء إلى تلك الدالة وينتقل إلى السطر الأول من الشيفرة البرمجية فيها. زر Step Over يؤدي النقر على زر "Step Over" إلى تنفيذ السطر التالي من الشيفرة البرمجية مثل زر "Step In"، ولكن إذا كان السطر التالي من الشيفرة البرمجية هو استدعاء دالة، فسيتجاوز زر "Step Over" الشيفرة البرمجية الموجودة في هذه الدالة، حيث ستُنفَّذ الشيفرة البرمجية الخاصة بالدالة بالسرعة القصوى، وسيتوقّف منقّح الأخطاء بعد العودة من استدعاء هذه الدالة. إذا استدعى السطر التالي من الشيفرة البرمجية الدالة spam() مثلًا، ولكنك لا تهتم بالشيفرة البرمجية الموجودة ضمن هذه الدالة، فيمكنك النقر على زر "Step Over" لتنفيذ الشيفرة البرمجية الموجودة في الدالة بالسرعة العادية ثم التوقف عندما تعود الدالة، لذلك يُعَد استخدام زر "Step Over" أكثر شيوعًا من استخدام زر "Step In". زر Step Out يؤدي النقر على زر "Step Out" إلى أن ينفّذ منقّح الأخطاء سطورًا من الشيفرة البرمجية بالسرعة القصوى حتى العودة من الدالة الحالية. إذا دخلتَ في استدعاء دالة باستخدام زر "Step In" وتريد الاستمرار في تنفيذ التعليمات حتى الخروج منها، فانقر على زر "Step Out" للخروج من استدعاء الدالة الحالي. زر التوقف Stop إذا أردتَ إيقاف تنقيح الأخطاء وعدم إزعاجك بمتابعة تنفيذ بقية البرنامج، فانقر فوق زر التوقّف "Stop"، حيث سيؤدي الزر "Stop" إلى إنهاء البرنامج مباشرةً. تنقيح أخطاء برنامج لجمع الأعداد افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد وأدخِل الشيفرة البرمجية التالية: print('Enter the first number to add:') first = input() print('Enter the second number to add:') second = input() print('Enter the third number to add:') third = input() print('The sum is ' + first + second + third) احفظ البرنامج بالاسم buggyAddingProgram.py وشغّله أولًا دون تفعيل منقّح الأخطاء، وسيكون خرج البرنامج كما يلي: Enter the first number to add: 5 Enter the second number to add: 3 Enter the third number to add: 42 The sum is 5342 لم يتعطل البرنامج، ولكن من الواضح أن ناتج الجمع خاطئ. شغّل البرنامج مرة أخرى ولكن مع منقّح الأخطاء هذه المرة، حيث إذا نقرتَ على زر تنقيح الأخطاء "Debug"، فسيتوقف البرنامج مؤقتًا عند السطر 1، وهو سطر الشيفرة البرمجية الذي نوشك على تنفيذه، إذ يجب أن يبدو المحرّر Mu كما في الشكل السابق. انقر على زر "Step Over" مرة واحدة لتنفيذ الاستدعاء الأول print()، إذ يجب أن تستخدم زر "Step Over" بدلًا من زر "Step In" هنا، لأنك لا تريد الدخول إلى الشيفرة البرمجية الخاصة بالدالة print()، بالرغم من أن المحرّر Mu يجب أن يمنع منقّح الأخطاء من الدخول إلى دوال بايثون المُدمَجة. ينتقل منقّح الأخطاء إلى السطر 2، ويميّز السطر 2 في محرر الملفات كما هو موضح في الشكل التالي، مما يوضّح لك مكان تنفيذ البرنامج حاليًا: نافذة المحرّر Mu بعد النقر على زر "Step Over" انقر على زر "Step Over" مرة أخرى لتنفيذ استدعاء الدالة input()، حيث سيختفي التمييز عن الشيفرة البرمجية أثناء انتظار المحرّر Mu أن تكتب شيئًا ما لاستدعاء الدالة input() في نافذة الخرج. أدخِل القيمة 5 واضغط على مفتاح ENTER، ثم سيعود التمييز إلى الشيفرة البرمجية. استمر في النقر على زر "Step Over"، وأدخِل القيمتين 3 و 42 بوصفهما العددين التاليين. يجب أن تبدو نافذة المحرّر Mu كما في الشكل التالي عندما يصل منقّح الأخطاء إلى السطر 7 الذي يمثّل استدعاء الدالة print() النهائي في البرنامج: توضّح نافذة فحص تنقيح الأخطاء Debug Inspector الموجودة على الجانب الأيمن أن المتغيرات مضبوطة بوصفها سلاسلًا نصية وليست أعدادًا صحيحة، مما تسبّب في حدوث الخطأ يجب أن ترى في نافذة فحص تنقيح الأخطاء Debug Inspector أن المتغيرات first و second و third مضبوطة بوصفها سلاسلًا نصية '5' و '3' و '42' بدلًا من الأعداد الصحيحة 5 و 3 و 42. تضم لغة بايثون هذه السلاسل النصية مع بعضها بعضًا عند تنفيذ السطر الأخير بدلًا من جمع هذه الأعداد، مما يتسبّب في حدوث الخطأ. يُعَد التنقل عبر البرنامج باستخدام منقّح الأخطاء بطيئًا، لذا نريد في أغلب الأحيان أن يعمل البرنامج بصورة طبيعية حتى يصل إلى سطر معين من الشيفرة البرمجية، حيث يمكنك ضبط منقّح الأخطاء لتطبيق ذلك باستخدام نقاط التوقف. نقاط التوقف Breakpoints يمكن ضبط نقطة توقف على سطر معين من الشيفرة البرمجية وإجبار منقح الأخطاء على التوقف مؤقتًا عندما يصل تنفيذ البرنامج إلى هذا السطر. افتح تبويبًا جديدًا في محرّر الملفات وأدخِل البرنامج التالي الذي يحاكي رمي عملة معدنية 1000 مرة، واحفظ البرنامج بالاسم coinFlip.py: import random heads = 0 for i in range(1, 1001): ➊ if random.randint(0, 1) == 1: heads = heads + 1 if i == 500: ➋ print('Halfway done!') print('Heads came up ' + str(heads) + ' times.') سيعيد الاستدعاء random.randint(0, 1) ➊ القيمة 0 في نصف الوقت والقيمة 1 في النصف الآخر من الوقت، حيث يمكن استخدام هذا الاستدعاء لمحاكاة رمي قطعة نقود وفق الاحتمال 50/50 وتمثل القيمة 1 الصورة من العملة المعدنية. يكون خرج هذا البرنامج كما يلي عند تشغيله بدون منقّح الأخطاء: Halfway done! Heads came up 490 times. إذا شغّلتَ هذا البرنامج مع منقّح الأخطاء، فيجب أن تنقر على زر "Step Over" آلاف المرات قبل إنهاء البرنامج. إذا كنت مهتمًا بالقيمة heads التي تمثّل صورة العملة المعدنية عند منتصف تنفيذ البرنامج أو عند اكتمال 500 مرة من 1000 مرة لرمي قطعة نقود، فيمكنك ضبط نقطة توقف على السطر print('Halfway done!') ➋، حيث يمكنك ضبط نقطة توقف من خلال النقر على رقم السطر في محرّر الملفات بحيث تظهر نقطة حمراء كما في الشكل التالي: يؤدي ضبط نقطة التوقف إلى ظهور نقطة حمراء (محاطة بدائرة) بجانب رقم السطر لا نريد ضبط نقطة توقف عند سطر التعليمة if لأنها تُنفَّذ في كل تكرار للحلقة، حيث إذا ضبطنا نقطة التوقف على الشيفرة البرمجية الموجودة ضمن التعليمة if، فسيتوقّف منقّح الأخطاء فقط عندما يدخل التنفيذ إلى هذه التعليمة. سيكون للسطر الذي يحتوي على نقطة التوقف نقطة حمراء بجانبه. إذا شغّلنا البرنامج مع منقّح الأخطاء، فسيبدأ في حالة التوقّف المؤقت عند السطر الأول كالمعتاد، ولكن إذا نقرت على زر المتابعة "Continue"، فسيُشغَّل البرنامج بالسرعة القصوى حتى يصل إلى السطر الذي ضبطنا نقطة التوقف عنده. يمكنك بعد ذلك النقر على أزرار "Continue" أو "Step Over" أو "Step In" أو "Step Out" للمتابعة كالمعتاد. إذا أردتَ إزالة نقطة توقف، فانقر على رقم السطر مرة أخرى، وستختفي النقطة الحمراء، ولن يتوقف منقّح الأخطاء عند هذا السطر لاحقًا. مشروع للتدريب حاول كتابة برنامج يطبّق ما يلي لكسب خبرة عملية أكبر. تنقيح أخطاء برنامج لرمي عملة معدنية يهدف هذا البرنامج إلى إنشاء لعبة تخمين بسيطة لرمي عملة معدنية مع حصول اللاعب على تخمينين، ولكنه يحتوي على العديد من الأخطاء بالرغم من سهولة هذه اللعبة. شغّل البرنامج عدة مرات للعثور على الأخطاء التي تمنع البرنامج من العمل بصورة صحيحة. import random guess = '' while guess not in ('heads', 'tails'): print('Guess the coin toss! Enter heads or tails:') guess = input() toss = random.randint(0, 1) # تمثّل القيمة 0 الكتابة، وتمثل القيمة 1 الصورة في العملة المعدنية if toss == guess: print('You got it!') else: print('Nope! Guess again!') guesss = input() if toss == guess: print('You got it!') else: print('Nope. You are really bad at this game.') الخلاصة تُعَد التأكيدات والاستثناءات والتسجيل ومنقح الأخطاء أدوات قيّمة للعثور على الأخطاء ومنعها في برنامجك، حيث تُعَد التأكيدات باستخدام التعليمة assert في لغة بايثون طريقة جيدة لتنفيذ فحص السلامة الذي يمنحك تحذيرًا مبكرًا عندما لا يكون الشرط الضروري صحيحًا، وتكون التأكيدات مخصَّصة فقط للأخطاء التي يجب ألّا يحاول البرنامج التعافي منها ويجب أن يفشل بسرعة، وإلّا فيجب أن ترفع استثناء. يمكن اكتشاف الاستثناء ومعالجته باستخدام تعليمات try و except. تُعَد الوحدة logging طريقة جيدة لتفحّص شيفرتك البرمجية أثناء تشغيلها، وهي أكثر ملاءمة للاستخدام من الدالة print() لاحتوائها على مستويات تسجيل متعددة ولقدرتها على التسجيل في ملف نصي. يتيح لك منقّح الأخطاء التنقل عبر برنامجك سطرًا تلو الآخر، ويمكنك تشغيل برنامجك بالسرعة العادية وجعل منقّح الأخطاء يوقف التنفيذ مؤقتًا عندما يصل إلى سطرٍ ضبطنا عنده نقطة توقف، ويمكنك رؤية حالة قيمة أيّ متغير في أي وقت من عمر البرنامج باستخدام منقّح الأخطاء. تساعدك هذه الأدوات والتقنيات لتنقيح الأخطاء على كتابة البرامج الناجحة، حيث يُعَد إدخال أخطاء في شيفرتك البرمجية عن طريق الخطأ حقيقة يجب التسليم بها بغض النظر عن عدد سنوات خبرتك في البرمجة. ترجمة -وبتصرُّف- للمقال Debugging لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: تنظيم الملفات باستخدام بايثون كتابة شيفرات بايثون: صيغ شائعة الاستخدام على نحو خاطئ أساسيات البرمجة بلغة بايثون النسخة الكاملة لكتاب البرمجة بلغة بايثون
-
تعلّمنا في مقالٍ سابق كيفية إنشاء ملفات جديدة والكتابة فيها باستخدام لغة بايثون Python، ويمكن لبرامجك أيضًا تنظيم الملفات الموجودة مسبقًا على القرص الصلب. لا بد أنك جرّبتَ تصفح مجلدٍ مليء بالعشرات أو المئات أو حتى الآلاف من الملفات ونسخها أو إعادة تسميتها أو نقلها أو ضغطها جميعًا يدويًا، أو جرّبتَ مهامًا أخرى مثل المهام التالية: إنشاء نُسخ من جميع ملفات PDF الموجودة في كل مجلدٍ فرعي من مجلدٍ ما. إزالة الأصفار البادئة في أسماء الملفات لكل ملفٍ في مجلد يضم مئات الملفات المسمّاة مثلًا spam001.txt و spam002.txt وإلخ. ضغط محتويات عدة مجلدات في ملف مضغوط ZIP واحد، والذي يمكن أن يكون نظام نسخ احتياطي بسيط. يمكنك تنفيذ كافة هذه المهام المملة آليًا في شيفرة بايثون البرمجية، فإذا برمجتَ حاسوبك لإجراء هذه المهام، فيمكنك تحويله إلى محرر ملفات سريع في العمل ولا يرتكب أخطاءً أبدًا. من المفيد رؤية امتداد الملف (مثل .txt و .pdf و .jpg وإلخ) بسرعة عند بدء العمل مع الملفات، إذ يُرجَّح أن يعرض متصفح ملفاتك الامتدادات تلقائيًا في نظامي ماك macOS ولينكس Linux، ولكن قد تكون امتدادات الملفات مخفية افتراضيًا في نظام ويندوز Windows، لذا يمكنك إظهار الامتدادات من خلال الانتقال إلى قائمة ابدأ Start، ثم لوحة التحكم Control Panel، ثم المظهر وإضفاء طابع شخصي Appearance and Personalization، ثم خيارات مستكشف الملفات Folder Options. ألغِ تحديد خانة الاختيار إخفاء ملحقات الملفات لأنواع الملفات المعروفة Hide extensions for known file types في تبويب عرض View ضمن الإعدادات المتقدمة Advanced Settings. وحدة shutil تحتوي وحدة shutil (التي هي اختصار لأدوات الصدفة المساعدة Shell Utilities) على دوالٍ تتيح لك نسخ الملفات ونقلها وإعادة تسميتها وحذفها في برامج بايثون الخاصة بك، ولكن يجب أولًا أن تستخدم التعليمة import shutil لاستخدام هذه الدوال. نسخ الملفات والمجلدات توفّر وحدة shutil دوالًا لنسخ الملفات والمجلدات الكاملة، حيث سيؤدي استدعاء الدالة shutil.copy(source, destination) إلى نسخ الملف من مسار المصدر source إلى المجلد الموجود في مسار الوِجهة destination، إذ يمكن أن يكون كلٌّ من source و destination سلاسلًا نصية أو كائنات Path. إذا كان destination اسم ملف، فسنستخدمه بوصفه اسمًا جديدًا للملف المنسوخ. تعيد هذه الدالة سلسلة نصية أو كائن Path للملف المنسوخ. أدخِل مثلًا ما يلي في الصدفة التفاعلية Interactive Shell لترى كيفية عمل الدالة shutil.copy(): >>> import shutil, os >>> from pathlib import Path >>> p = Path.home() ➊ >>> shutil.copy(p / 'spam.txt', p / 'some_folder') 'C:\\Users\\Al\\some_folder\\spam.txt' ➋ >>> shutil.copy(p / 'eggs.txt', p / 'some_folder/eggs2.txt') WindowsPath('C:/Users/Al/some_folder/eggs2.txt') ينسخ استدعاء الدالة shutil.copy() الأول الملف الموجود في C:\Users\Al\spam.txt إلى المجلد C:\Users\Al\some_folder، وتكون القيمة المُعادة هي مسار الملف المنسوخ، ولاحظ استخدام اسم الملف spam.txt الأصلي لاسم الملف المنسوخ الجديد عند تحديد مجلدٍ بوصفه الوِجهة ➊. ينسخ استدعاء الدالة shutil.copy() الثاني ➋ الملف الموجود في C:\Users\Al\eggs.txt إلى المجلد C:\Users\Al\some_folder، ولكنه يعطي الملف المنسوخ الاسم eggs2.txt. تنسخ الدالة shutil.copy() ملفًا واحدًا، وتنسخ الدالة shutil.copytree() مجلدًا كاملًا مع جميع المجلدات والملفات الموجودة فيه، حيث يؤدي استدعاء الدالة shutil.copytree(source, destination) إلى نسخ المجلد الموجود في مسار المصدر source مع جميع ملفاته ومجلداته الفرعية إلى المجلد الموجود في مسار الوِجهة destination، إذ تكون المعاملات source و destination سلاسلًا نصية. تعيد هذه الدالة سلسلة نصية تمثّل مسار المجلد المنسوخ. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import shutil, os >>> from pathlib import Path >>> p = Path.home() >>> shutil.copytree(p / 'spam', p / 'spam_backup') WindowsPath('C:/Users/Al/spam_backup') ينشئ استدعاء الدالة shutil.copytree() السابق مجلدًا جديدًا بالاسم spam_backup الذي يحتوي على محتوى المجلد spam الأصلي نفسه، وبالتالي سنحصل بأمان على نسخة احتياطية من المجلد spam. نقل وإعادة تسمية الملفات والمجلدات سيؤدي استدعاء الدالة shutil.move(source, destination) إلى نقل الملف أو المجلد الموجود في مسار المصدر source إلى مسار الوجهة destination، وسيعيد سلسلة نصية للمسار المطلق الخاص بالموقع الجديد. إذا أشار مسار الوجهة destination إلى مجلد، فسيُنقَل ملف المصدر source إلى الوجهة destination ويحتفظ باسم الملف الحالي. لندخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import shutil >>> shutil.move('C:\\beef.txt', 'C:\\eggs') 'C:\\eggs\\beef.txt' لنفترض أن المجلد الذي اسمه eggs موجودٌ مسبقًا في المجلد C:\، فسيمثّل استدعاء الدالة shutil.move() نقلَ الملف C:\beef.txt إلى المجلد C:\eggs. إذا كان الملف beef.txt موجودًا مسبقًا في المجلد C:\eggs، فسيُكتَب فوقه، لذا يجب عليك توخي الحذر عند استخدام الدالة move() لأنه من السهل الكتابة فوق الملفات عن طريق الخطأ باستخدام هذه الطريقة. يمكن لمسار الوِجهة destination أيضًا تحديد اسم الملف، حيث سننقل ملف المصدر source ونعيد تسميته في المثال التالي: >>> shutil.move('C:\\beef.txt', 'C:\\eggs\\new_beef.txt') 'C:\\eggs\\new_beef.txt' يمثّل السطر السابق نقل الملف C:\beef.txt إلى المجلد C:\eggs وإعادة تسميته بالاسم new_beef.txt. نفترض في المثالين السابقين وجود المجلد eggs في المجلد C:\، ولكن إن لم يوجَد المجلد eggs، فستعيد الدالة move() تسمية الملف beef.txt إلى ملفٍ اسمه eggs. >>> shutil.move('C:\\beef.txt', 'C:\\eggs') 'C:\\eggs' لم تتمكّن الدالة move() من العثور على مجلدٍ بالاسم eggs في المجلد C:\، وبالتالي تفترض أن الوجهة destination يجب أن تحدد اسم ملفٍ وليس اسم مجلد، لذلك أُعيدت تسمية الملف النصي beef.txt إلى egg (ملف نصي بدون امتداد الملف .txt)، وربما ليس هذا ما أردته. يمكن أن يكون ذلك خطأً يصعب اكتشافه في برامجك، لأن استدعاء الدالة move() يمكن أن يفعل شيئًا قد يكون مختلفًا تمامًا عمّا تتوقعه، وهذا سبب آخر لتوخي الحذر عند استخدام الدالة move(). أخيرًا، يجب أن تكون المجلدات التي تشكّل الوِجهة موجودة فعليًا، وإلّا فسترمي شيفرة بايثون استثناءً. لندخِل الآن ما يلي في الصدفة التفاعلية: >>> shutil.move('spam.txt', 'c:\\does_not_exist\\eggs\\meat') Traceback (most recent call last): --snip-- FileNotFoundError: [Errno 2] No such file or directory: 'c:\\does_not_exist\\ eggs\\meat' تبحث شيفرة بايثون عن المجلدين eggs و meat ضمن المجلد does_not_exist، ولكنها لا تعثر على هذا المجلد، لذا لا يمكنها نقل الملف spam.txt إلى المسار الذي حدّدته. حذف الملفات والمجلدات نهائيًا يمكنك حذف ملف واحد أو مجلد واحد فارغ باستخدام دوال تابعة للوحدة os، ولكن يمكنك حذف مجلدٍ وجميع محتوياته باستخدام الوحدة shutil، وهذه الدوال هي: سيؤدي استدعاء الدالة os.unlink(path) إلى حذف الملف الموجود في المسار path. سيؤدي استدعاء الدالة os.rmdir(path) إلى حذف المجلد الموجود في المسار path، ولكن يجب أن يكون هذا المجلد خاليًا من أي ملفات أو مجلدات. سيؤدي استدعاء الدالة shutil.rmtree(path) إلى إزالة المجلد الموجود في المسار path، وستُحذَف جميع الملفات والمجلدات الموجودة ضمنه. كن حذرًا عند استخدام هذه الدوال في برامجك، إذ من الجيد أن تشغّل برنامجك أولًا مع تعليق هذه الاستدعاءات وإضافة استدعاءات الدالة print() لإظهار الملفات التي ستُحذَف. إليك فيما يلي برنامج بايثون الذي يهدف إلى حذف الملفات التي لها امتداد الملف .txt، ولكن يوجد به خطأ مطبعي يؤدي إلى حذف ملفات .rxt بدلًا من ذلك: import os from pathlib import Path for filename in Path.home().glob('*.rxt'): os.unlink(filename) إذا كان لديك أيّ ملفات مهمة تنتهي بالامتداد .rxt فستُحذَف نهائيًا عن طريق الخطأ، لذا يجب أولًا أن تشغّل البرنامج كما يلي: import os from pathlib import Path for filename in Path.home().glob('*.rxt'): #os.unlink(filename) print(filename) لاحظ أننا علّقنا استدعاء الدالة os.unlink()، لذا تجاهلته شيفرة بايثون، وستطبع اسم الملف المحذوف فقط، حيث سيؤدي تشغيل هذه النسخة من البرنامج أولًا إلى إظهار أنك طلبت من البرنامج عن طريق الخطأ حذف ملفات .rxt بدلًا من ملفات .txt. تأكّد من عمل البرنامج بالطريقة الصحيحة، ثم احذف سطر التعليمة print(filename) وألغِ التعليق عند سطر التعليمة os.unlink(filename)، ثم شغّل البرنامج مرة أخرى لحذف الملفات فعليًا. الحذف الآمن باستخدام وحدة send2trash تحذف الدالة shutil.rmtree() المُدمَجة مع لغة بايثون الملفات والمجلدات نهائيًا، ولكن قد يكون استخدامها خطيرًا، لذا توجد طريقة أفضل بكثير لحذف الملفات والمجلدات، وهي استخدام الوحدة send2trash الخارجية. يمكنك تثبيت هذه الوحدة من خلال تشغيل الأمر pip install --user send2trash من نافذة الطرفية Terminal. يُعَد استخدام الوحدة send2trash أكثر أمانًا من دوال الحذف العادية الخاصة بلغة بايثون، لأنها سترسل المجلدات والملفات إلى سلة المهملات أو سلة المحذوفات الخاصة بحاسوبك بدلًا من حذفها نهائيًا. إذا أدّى خطأٌ ما في برنامجك إلى حذفٍ شيءٍ باستخدام الوحدة send2trash ولا تريد حذفه، فيمكنك استعادته من سلة المحذوفات لاحقًا. أدخِل مثلًا ما يلي في الصدفة التفاعلية بعد تثبيت الوحدة send2trash: >>> import send2trash >>> beefFile = open('beef.txt', 'a') # إنشاء الملف >>> beefFile.write('Beef is not a vegetable.') 25 >>> beefFile.close() >>> send2trash.send2trash('beef.txt') يجب دائمًا أن تستخدم الدالة send2trash.send2trash() لحذف الملفات والمجلدات، ولكن بالرغم من أن إرسال الملفات إلى سلة المحذوفات يتيح لك استعادتها لاحقًا، إلّا أنه لن يؤدي إلى تحرير مساحةٍ من القرص الصلب كما يفعل الحذف النهائي، لذا إذا أردتَ أن يحرّر برنامجك مساحةً من القرص الصلب، فاستخدم دوال الوحدتين os و shutil لحذف الملفات والمجلدات. لاحظ أن الدالة send2trash() يمكنها إرسال الملفات إلى سلة المحذوفات فقط، ولا يمكنها سحب الملفات منها. المرور على شجرة مجلدات لنفترض أنك تريد إعادة تسمية كل ملف في مجلدٍ ما وكل ملفٍ في كل مجلدٍ فرعي من هذا المجلد، وهذا يعني أنك تريد المرور على شجرة المجلدات، والتفاعل مع جميع الملفات أثناء المرور عليها. قد تكون كتابة برنامجٍ لذلك أمرًا صعبًا، ولكن توفّر بايثون الدالة os.walk() للتعامل مع هذه العملية نيابةً عنك. أولًا، لنلقِ نظرة على المجلد C:\delicious ومحتوياته كما هو موضح في الشكل التالي: مثال لمجلد يحتوي على ثلاثة مجلدات وأربعة ملفات إليك فيما يلي مثال لبرنامج يستخدم الدالة os.walk() مع شجرة المجلدات من الشكل السابق: import os for folderName, subfolders, filenames in os.walk('C:\\delicious'): print('The current folder is ' + folderName) for subfolder in subfolders: print('SUBFOLDER OF ' + folderName + ': ' + subfolder) for filename in filenames: print('FILE INSIDE ' + folderName + ': '+ filename) print('') نمرّر قيمة سلسلة نصية واحدة تمثّل مسار المجلد إلى الدالة os.walk() التي يمكنك استخدامها في تعليمة حلقة for للمرور على شجرة المجلدات، حيث يشبه ذلك استخدام الدالة range() للمرور على مجالٍ من الأعداد، ولكن ستعيد الدالة os.walk() ثلاث قيم في كل تكرار من هذه الحلقة، وهذه القيم هي: سلسلة نصية تمثّل اسم المجلد الحالي. قائمة من السلاسل النصية التي تمثّل المجلدات الموجودة في المجلد الحالي. قائمة من السلاسل النصية التي تمثّل الملفات الموجودة في المجلد الحالي. ملاحظة: المجلد الحالي هو المجلد الخاص بالتكرار الحالي لحلقة for، ولم تغيّر الدالة os.walk() مجلد العمل الحالي للبرنامج. يمكنك اختيار اسم المتغير i في شيفرة for i in range(10):، ويمكنك أيضًا اختيار أسماء المتغيرات للقيم الثلاث المذكورة سابقًا، ولكننا سنستخدم أسماء المتغيرات foldername و subfolders و filenames في أغلب الأحيان. إذا شغّلنا البرنامج، فسينتج ما يلي: The current folder is C:\delicious SUBFOLDER OF C:\delicious: cats SUBFOLDER OF C:\delicious: walnut FILE INSIDE C:\delicious: spam.txt The current folder is C:\delicious\cats FILE INSIDE C:\delicious\cats: catnames.txt FILE INSIDE C:\delicious\cats: zophie.jpg The current folder is C:\delicious\walnut SUBFOLDER OF C:\delicious\walnut: waffles The current folder is C:\delicious\walnut\waffles FILE INSIDE C:\delicious\walnut\waffles: butter.txt. تعيد الدالة os.walk() قوائمًا من السلاسل النصية التي تمثّل المتغيرات subfolder و filename، لذا يمكنك استخدام هذه القوائم في حلقات for الخاصة بها. ضع شيفرتك البرمجية مكان استدعاءات الدالة print()، أو احذف حلقتي for إن لم تكن بحاجة إليهما. ضغط الملفات باستخدام الوحدة zipfile قد تكون على دراية بالملفات المضغوطة ZIP ذات امتداد الملف .zip، والتي يمكنها الاحتفاظ بالمحتويات المضغوطة للعديد من الملفات الأخرى، حيث يؤدي ضغط الملف إلى تقليل حجمه، ويُعَد ذلك أمرًا مفيدًا عند نقله عبر الإنترنت. يمكن أن يحتوي ملف ZIP أيضًا على ملفات ومجلدات فرعية متعددة، لذا تُعَد طريقة سهلة لحَزم ملفات متعددة في ملف واحد، ويمكن بعد ذلك مثلًا إرفاق هذا الملف الذي يسمّى ملف الأرشفة Archive File مع رسالة بريد إلكتروني. يمكن لبرامج بايثون الخاص بك إنشاء ملفات ZIP وفتحها (أو فك ضغطها Extract) باستخدام الدوال الموجودة في الوحدة zipfile. لنفترض أن لديك ملف ZIP بالاسم example.zip ويحتوي على المحتويات الموضّحة في الشكل التالي: محتويات الملف example.zip يمكنك تنزيل هذا الملف من موقع nostarch أو المتابعة باستخدام ملف ZIP موجود مسبقًا على حاسوبك. قراءة الملفات المضغوطة ZIP يمكنك قراءة محتويات ملف مضغوط ZIP من خلال إنشاء كائن ZipFile أولًا (لاحظ الأحرف الكبيرة Z و F)، حيث تتشابه كائنات ZipFile مع كائنات File التي تعيدها الدالة open()، فهي قيم يتفاعل البرنامج من خلالها مع الملف. ننشئ كائن ZipFile من خلال استدعاء الدالة zipfile.ZipFile() وتمرير سلسلة نصية تمثّل اسم ملف .ZIP إليها. لاحظ أن zipfile هو اسم وحدة بايثون، وأن ZipFile() هو اسم الدالة. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import zipfile, os >>> from pathlib import Path >>> p = Path.home() >>> exampleZip = zipfile.ZipFile(p / 'example.zip') >>> exampleZip.namelist() ['spam.txt', 'cats/', 'cats/catnames.txt', 'cats/zophie.jpg'] >>> spamInfo = exampleZip.getinfo('spam.txt') >>> spamInfo.file_size 13908 >>> spamInfo.compress_size 3828 ➊ >>> f'Compressed file is {round(spamInfo.file_size / spamInfo .compress_size, 2)}x smaller!' ) 'Compressed file is 3.63x smaller!' >>> exampleZip.close() يحتوي كائن ZipFile على التابع namelist() الذي يعيد قائمةً من السلاسل النصية التي تمثّل جميع الملفات والمجلدات الموجودة في ملف ZIP، حيث يمكن تمرير هذه السلاسل النصية إلى التابع getinfo() لإعادة كائن ZipInfo لهذا الملف المحدّد. تمتلك كائنات ZipInfo سماتها Attributes الخاصة مثل السمات file_size و compress_size بالبايتات، والتي تحتوي على أعداد صحيحة لحجم الملف الأصلي وحجم الملف المضغوط على التوالي. يمثل كائن ZipInfo ملف أرشفة كامل، ولكن يحمل كائن ZipInfo أيضًا معلومات مفيدة حول ملف واحد في ملف الأرشفة. يحسب الأمر الموجود في التعليمة ➊ مدى كفاءة ضغط الملف example.zip من خلال قسمة حجم الملف الأصلي على حجم الملف المضغوط ويطبع هذه المعلومات. فك ضغط ملفات ZIP يفك التابع extractall() الخاص بكائنات ZipFile ضغط جميع الملفات والمجلدات من ملف مضغوط ZIP إلى مجلد العمل الحالي. >>> import zipfile, os >>> from pathlib import Path >>> p = Path.home() >>> exampleZip = zipfile.ZipFile(p / 'example.zip') ➊ >>> exampleZip.extractall() >>> exampleZip.close() يؤدي تشغيل الشيفرة البرمجية السابقة إلى فَك ضغط محتويات الملف example.zip في المجلد C:\. يمكنك اختياريًا تمرير اسم مجلد إلى التابع extractall() لفك ضغط الملفات في مجلد آخر مختلفٍ عن مجلد العمل الحالي، وإذا كان المجلد الذي مرّرناه إلى التابع extractall() غير موجود، فسيُشَأ هذا المجلد، فمثلًا إذا وضعتَ الاستدعاء exampleZip.extractall('C:\\delicious') مكان الاستدعاء ➊، فستفك الشيفرة البرمجية ضغط الملفات من الملف example.zip إلى المجلد C:\delicious الذي أنشأناه. يفك التابع extract() الخاص بكائنات ZipFile ضغط ملفٍ واحد من الملف المضغوط ZIP. تابع مثال الصدفة التفاعلية بما يلي: >>> exampleZip.extract('spam.txt') 'C:\\spam.txt' >>> exampleZip.extract('spam.txt', 'C:\\some\\new\\folders') 'C:\\some\\new\\folders\\spam.txt' >>> exampleZip.close() يجب أن تتطابق السلسلة النصية التي تمررها إلى التابع extract() مع إحدى السلاسل النصية الموجودة في القائمة التي يعيدها التابع namelist()، ويمكنك اختياريًا تمرير وسيطٍ ثانٍ إلى التابع extract() لفك ضغط الملف في مجلد آخر مختلف عن مجلد العمل الحالي، حيث إذا كان هذا الوسيط الثاني مجلدًا غير موجودٍ بعد، فستنشِئ شيفرة بايثون هذا المجلد. القيمة التي يعيدها التابع extract() هي المسار المطلق الذي فكينا ضغط الملف فيه. إنشاء ملفات ZIP والإضافة إليها يمكنك إنشاء ملفات ZIP المضغوطة من خلال فتح كائن ZipFile في وضع الكتابة مع تمرير 'w' كوسيط ثانٍ، حيث يشبه ذلك فتح ملفٍ نصي في وضع الكتابة من خلال تمرير 'w' إلى الدالة open(). إذا مرّرتَ مسارًا إلى التابع write() مع كائن ZipFile، ستضغط شيفرة بايثون الملف الموجود في هذا المسار وتضيفه إلى ملف ZIP. الوسيط الأول للتابع write() هو سلسلة نصية تمثّل اسم الملف المراد إضافته، والوسيط الثاني هو معامل يمثّل نوع عملية الضغط، إذ يخبر هذا النوع الحاسوبَ بالخوارزمية التي يجب أن يستخدمها لضغط الملفات، حيث يمكنك دائمًا ضبط هذه القيمة على zipfile.ZIP_DEFLATED التي تحدّد خوارزمية الضغط Deflate التي تعمل على جميع أنواع البيانات. أدخِل مثلًا ما يلي في الصدفة التفاعلية: >>> import zipfile >>> newZip = zipfile.ZipFile('new.zip', 'w') >>> newZip.write('spam.txt', compress_type=zipfile.ZIP_DEFLATED) >>> newZip.close() ستؤدي الشيفرة البرمجية السابقة إلى إنشاء ملف ZIP جديد بالاسم new.zip، حيث يحتوي هذا الملف على محتويات مضغوطة للملف spam.txt. ضع في بالك أن وضع الكتابة سيؤدي إلى مسح جميع المحتويات الموجودة مسبقًا في ملف ZIP كما هو الحال مع الكتابة في الملفات. إذا أردتَ ببساطة إضافة ملفات إلى ملف ZIP موجود مسبقًا، فمرّر 'a' كوسيطٍ ثانٍ إلى الدالة zipfile.ZipFile() لفتح ملف ZIP في وضع الإلحاق Append Mode. تطبيق عملي: إعادة تسمية الملفات ذات تواريخ النمط الأمريكي إلى تواريخ النمط الأوروبي لنفترض أن مديرك في العمل يرسل إليك عبر البريد الإلكتروني آلاف الملفات ذات تواريخ النمط الأمريكي (MM-DD-YYYY) الموجودة في أسماء هذه الملفات ويريد إعادة تسميتها إلى تواريخ النمط الأوروبي (DD-MM-YYYY)، إذ قد يستغرق إنجاز هذه المهمة المملة يدويًا وقتًا طويلًا، إذًا لنكتب برنامجًا ينّفذ هذه المهمة نيابةً عنك. إليك الخطوات التي يفعلها هذا البرنامج: البحث في جميع أسماء الملفات الموجودة في مجلد العمل الحالي عن التواريخ ذات النمط الأمريكي. إعادة تسمية الملف مع التبديل بين الشهر واليوم لجعله على النمط الأوروبي عند العثور على أحد هذه الملفات. وبالتالي يجب أن تطبّق شيفرتك البرمجية الخطوات التالية: إنشاء تعبير نمطي Regex يمكنه تحديد نمط النص للتواريخ ذات النمط الأمريكي. استدعاء التابع os.listdir() للعثور على جميع الملفات الموجودة في مجلد العمل. المرور ضمن حلقة على جميع أسماء الملفات باستخدام التعبير النمطي للتحقق من احتوائه على تاريخ. إذا احتوى اسم الملف على تاريخ، فيجب إعادة تسمية الملف باستخدام الدالة shutil.move(). افتح نافذةً جديدة في محرّرك لإنشاء ملف جديد للمشروع واحفظ شيفرتك البرمجية بالاسم renameDates.py. الخطوة الأولى: إنشاء تعبير نمطي للتواريخ ذات النمط الأمريكي سيحتاج الجزء الأول من البرنامج إلى استيراد الوحدات الضرورية وإنشاء تعبير نمطي يمكنه تحديد تواريخ النمط الأمريكي MM-DD-YYYY. ستذكرك تعليقات TODO في النهاية بما تبقى لتكتبه في هذا البرنامج، حيث كتبناها ليسهل عليك العثور عليها باستخدام ميزة البحث Ctrl-F في محرّر Mu. اجعل شيفرتك البرمجية تبدو كما يلي: #! python3 # renameDates.py - إعادة تسمية أسماء الملفات ذات تنسيق التاريخ الأمريكي MM-DD-YYYY إلى # تنسيق التاريخ الأوروبي DD-MM-YYYY ➊ import shutil, os, re # إنشاء تعبير نمطي يطابق الملفات ذات تنسيق التاريخ الأمريكي ➋ datePattern = re.compile(r"""^(.*?) # كل النص قبل التاريخ ((0|1)?\d)- # رقم أو رقمين للشهر ((0|1|2|3)?\d)- # رقم أو رقمين لليوم ((19|20)\d\d) # أربعة أرقام للسنة (.*?)$ # كل النص بعد التاريخ """, re.VERBOSE➌) # TODO: المرور ضمن حلقة على الملفات الموجودة في مجلد العمل # TODO: تخطي الملفات التي تكون بدون تاريخ # TODO: الحصول على الأجزاء المختلفة من اسم الملف # TODO: تشكيل اسم الملف على النمط الأوروبي # TODO: الحصول على مسارات الملفات الكاملة والمطلقة # TODO: إعادة تسمية الملفات تعلّمنا في هذا المقال أنه يمكن استخدام الدالة shutil.move() لإعادة تسمية الملفات، ووسطاؤها هي اسم الملف المراد إعادة تسميته واسم الملف الجديد، ويجب استيراد الوحدة shutil ➊ بسبب وجود هذه الدالة فيها. يجب تحديد الملفات التي تريد إعادة تسميتها قبل إعادة تسميتها، إذ يجب إعادة تسمية أسماء الملفات التي لها تواريخ مثل spam4-4-1984.txt و 01-03-2014eggs.zip، بينما يمكن تجاهل أسماء الملفات التي لا تحتوي على تواريخ مثل littlebrother.epub. يمكنك استخدام تعبير نمطي لتحديد هذا النمط، لذا استدعِ التابع re.compile() لإنشاء كائن Regex ➋ بعد استيراد الوحدة re في البداية. سيسمح تمرير القيمة re.VERBOSE للوسيط الثاني ➌ بوجود المسافات البيضاء والتعليقات في السلسلة النصية للتعبير النمطي لجعلها أكثر قابلية للقراءة. تبدأ السلسلة النصية للتعبير النمطي بالمحارف ^(.*?) لمطابقة أيّ نصٍ موجود في بداية اسم الملف الذي قد يأتي قبل التاريخ. تطابق المجموعة ((0|1)?\d) الشهر، حيث يمكن أن يكون الرقم الأول إما 0 أو 1، وبالتالي يطابق التعبير النمطي القيمةَ 12 للشهر 12 ويطابق القيمة 02 للشهر الثاني، حيث يكون هذا الرقم اختياريًا أيضًا بحيث يمكن أن يكون الشهر 04 أو 4 للشهر الرابع. مجموعة اليوم هي ((0|1|2|3)?\d) وتتبع منطقًا مشابهًا لمجموعة الشهر، حيث تُعَد القيم 3 و 03 و 31 أرقامًا صالحة للأيام. لاحظ أن هذا التعبير النمطي سيقبل بعض التواريخ غير الصالحة مثل 4-31-2022 و 2-29-2023 و 0-15-2024، إذ تحتوي التواريخ على الكثير من الحالات الخاصة التي يمكن أن نخطئ بها بسهولة، ولكن يعمل التعبير النمطي في هذا البرنامج جيدًا بما فيه الكفاية للتبسيط. تُعَد السنة 1885 سنةً صالحة، ولكن يمكنك فقط البحث عن السنوات في القرن العشرين أو الحادي والعشرين، مما سيؤدي إلى منع برنامجك من مطابقة أسماء الملفات التي لها تنسيق مشابه للتاريخ ولكنها لا تمثّل تواريخًا مثل 10-10-1000.txt عن طريق الخطأ. أخيرًا، يتطابق الجزء $(?*.) من التعبير النمطي مع أيّ نص يأتي بعد التاريخ. الخطوة الثانية: تحديد أجزاء التاريخ من أسماء الملفات يجب بعد ذلك أن يمر البرنامج ضمن حلقة على قائمة السلاسل النصية لأسماء الملفات التي يعيدها التابع os.listdir()، وأن يطابقها مع التعبير النمطي. يجب تخطي أيّ ملفات لا تتضمن تاريخًا في اسمها، وسيُخزَّن النص المطابق في عدة متغيرات بالنسبة لأسماء الملفات التي تحتوي على تاريخ فيها. املأ المهام TODO الثلاثة الأولى في برنامجك بالشيفرة البرمجية التالية: #! python3 # renameDates.py - إعادة تسمية أسماء الملفات ذات تنسيق التاريخ الأمريكي MM-DD-YYYY إلى # تنسيق التاريخ الأوروبي DD-MM-YYYY --snip-- # المرور ضمن حلقة على الملفات الموجودة في مجلد العمل for amerFilename in os.listdir('.'): mo = datePattern.search(amerFilename) # تخطي الملفات التي تكون بدون تاريخ ➊ if mo == None: ➋ continue ➌ # الحصول على الأجزاء المختلفة من اسم الملف beforePart = mo.group(1) monthPart = mo.group(2) dayPart = mo.group(4) yearPart = mo.group(6) afterPart = mo.group(8) --snip– إذا كانت قيمة كائن Match الذي يعيده التابع search() هي None ➊، فلن يتطابق اسم الملف الموجود في المتغير amerFilename مع التعبير النمطي، وستتخطى تعليمة continue ➋ بقية الحلقة وتنتقل إلى اسم الملف التالي، وإلّا فستُخزَّن السلاسل النصية المختلفة المطابِقة مع مجموعات التعبير النمطي في متغيرات بالاسم beforePart و monthPart و dayPart و yearPart و afterPart ➌. ستُستخدَم السلاسل النصية الموجودة في هذه المتغيرات لتشكيل اسم الملف على النمط الأوروبي في الخطوة التالية. أبقِ أرقام المجموعة سهلة الاستخدام من خلال محاولة قراءة التعبير النمطي من البداية وحساب كل مرة يظهر فيها قوس مفتوح. اكتب مخططًا تفصيليًا للتعبير النمطي دون التفكير في الشيفرة البرمجية، حيث يمكن أن يساعدك المثال التالي في تصوّر هذه المجموعات: datePattern = re.compile(r"""^(1) # كل النص قبل التاريخ (2 (3) )- # رقم أو رقمين للشهر (4 (5) )- # رقم أو رقمين لليوم (6 (7) ) # أربعة أرقام للسنة (8)$ # كل النص بعد التاريخ """, re.VERBOSE) تمثل الأرقام من 1 إلى 8 في المثال السابق المجموعات في التعبير النمطي الذي كتبته. يمكن أن يمنحكَ إنشاء مخطط تفصيلي للتعبير النمطي باستخدام الأقواس وأرقام المجموعات فقط فهمًا أوضح لتعبيرك النمطي قبل الانتقال إلى بقية البرنامج. الخطوة الثالثة: تشكيل اسم الملف الجديد وإعادة تسمية الملفات جرّب سَلسَلة السلاسل النصية الموجودة في المتغيرات من الخطوة السابقة مع التاريخ ذي النمط الأوروبي، حيث يأتي اليوم قبل الشهر. املأ المهام TODO الثلاثة المتبقية في برنامجك بالشيفرة البرمجية التالية: #! python3 # renameDates.py - إعادة تسمية أسماء الملفات ذات تنسيق التاريخ الأمريكي MM-DD-YYYY إلى # تنسيق التاريخ الأوروبي DD-MM-YYYY --snip-- # تشكيل اسم الملف على النمط الأوروبي ➊ euroFilename = beforePart + dayPart + '-' + monthPart + '-' + yearPart + afterPart # الحصول على مسارات الملفات الكاملة والمطلقة absWorkingDir = os.path.abspath('.') amerFilename = os.path.join(absWorkingDir, amerFilename) euroFilename = os.path.join(absWorkingDir, euroFilename) # إعادة تسمية الملفات ➋ print(f'Renaming "{amerFilename}" to "{euroFilename}"...') ➌ #shutil.move(amerFilename, euroFilename) # ألغِ التعليق بعد الاختبار خزّن السلسلة النصية المتسلسلة في متغير بالاسم euroFilename ➊، ثم مرّر اسم الملف الأصلي الموجود في المتغير amerFilename والمتغير euroFilename الجديد إلى الدالة shutil.move() لإعادة تسمية الملف ➌. يتضمن هذا البرنامج تعليقًا على استدعاء الدالة shutil.move()، حيث يطبع أسماء الملفات التي ستُعاد تسميتها ➋. يمكن أن يتيح لك تشغيل البرنامج بهذه الطريقة أولًا التحققَ من إعادة تسمية الملفات بطريقة صحيحة، ثم يمكنك إلغاء تعليق استدعاء الدالة shutil.move() وتشغيل البرنامج مرة أخرى لإعادة تسمية الملفات فعليًا. أفكار لبرامج مماثلة هناك العديد من الأسباب الأخرى التي قد تجعلك ترغب في إعادة تسمية عدد كبير من الملفات مثل: إضافة بادئة إلى بداية اسم الملف مثل إضافة spam_ لإعادة تسمية الملف eggs.txt إلى الاسم spam_eggs.txt. تغيير أسماء الملفات التي تحتوي على تواريخ ذات نمط أوروبي إلى تواريخ ذات نمط الأمريكي. حذف الأصفار من أسماء الملفات مثل spam0042.txt. تطبيق عملي: إنشاء نسخة احتياطية لمجلد في ملف مضغوط ZIP لنفترض أنك تعمل على مشروع تحتفظ بملفاته في مجلد بالاسم C:\AlsPythonBook، ولا بد أنك قلق بشأن فقدان عملك، لذا سترغب في إنشاء "لقطات" من ملفات ZIP للمجلد بأكمله، إذ قد ترغب في الاحتفاظ بنسخ مختلفة، لذلك يجب أن يزيد اسم ملف ZIP في كل مرة تنشئ فيها نسخة مثل AlsPythonBook_1.zip و AlsPythonBook_2.zip و AlsPythonBook_3.zip وإلخ. يمكنك إنجاز ذلك يدويًا، ولكنه أمر مزعج إلى حدٍ ما، وقد تخطئ في ترقيم أسماء ملفات ZIP، فمن الأسهل تشغيل برنامج ينجز هذه المهمة المملة نيابةً عنك. افتح نافذة جديدة في محرّرك لإنشاء ملف جديد لهذا المشروع واحفظه بالاسم backupToZip.py. الخطوة الأولى: اكتشاف اسم الملف المضغوط ZIP سنضع الشيفرة البرمجية الخاصة بهذا البرنامج في دالة اسمها backupToZip()، حيث سيؤدي ذلك إلى تسهيل نسخ الدالة ولصقها في برامج بايثون الأخرى التي تحتاج إليها. ستُستدعَى هذه الدالة لإجراء النسخ الاحتياطي في نهاية البرنامج، لذا اجعل برنامجك يبدو كما يلي: #! python3 # backupToZip.py - نسخ مجلد كامل ومحتوياته في ملف ZIP يزيد اسمه بمقدار واحد في كل مرة يُنسَخ فيها ➊ import zipfile, os def backupToZip(folder): # إنشاء نسخة احتياطية من محتويات المجلد بالكامل في ملف ZIP folder = os.path.abspath(folder) # التأكد من أن المجلد مسار مطلق # اكتشاف اسم الملف الذي يجب أن تستخدمه هذه الشيفرة البرمجية بناءً على الملفات الموجودة مسبقًا ➋ number = 1 ➌ while True: zipFilename = os.path.basename(folder) + '_' + str(number) + '.zip' if not os.path.exists(zipFilename): break number = number + 1 ➍ # TODO: إنشاء ملف مضغوط ZIP # TODO: المرور على شجرة المجلدات بأكملها وضغط الملفات الموجودة في كل مجلد print('Done.') backupToZip('C:\\delicious') أضِف أولًا سطر Shebang (الذي يبدأ بالسلسلة النصية !#) مع وصف ما يفعله البرنامج، ثم استورد وحدات zipfile و os ➊. عرّف بعد ذلك دالة بالاسم backupToZip()، حيث تأخذ هذه الدالة معاملًا واحدًا فقط هو folder، والذي هو سلسلة نصية تمثّل مسارًا إلى المجلد الذي يجب نسخ محتوياته احتياطيًا. ستحدّد هذه الدالة اسم الملف المُستخدَم لملف ZIP الذي ستنشئه، ثم تنشئ هذه الدالة الملف، وتمر على المجلد folder، وتضيف كلًا من المجلدات الفرعية والملفات إلى ملف ZIP. اكتب تعليقات TODO لهذه الخطوات في الشيفرة البرمجية لتذكير نفسك بإنجازها لاحقًا ➍. يستخدم الجزء الأول -الذي يمثّل تسمية الملف ZIP- الاسم الأساسي للمسار المطلق للمجلد folder. إذا كان المجلد الذي ننسخه احتياطيًا هو C:\delicious، فيجب أن يكون اسم الملف ZIP هو delicious_N.zip، حيث N = 1 هي المرة الأولى التي نشغّل فيها البرنامج و N = 2 هي المرة الثانية وإلخ. يمكنك تحديد ما يجب أن تكون عليه قيمة N من خلال التحقق مما إذا كان الملف delicious1.zip موجودًا مسبقًا، ثم التحقق مما إذا كان الملف delicious2.zip موجودًا مسبقًا وإلخ. استخدم متغيرًا اسمه number لتمثيل N ➋، واستمر في زيادته ضمن الحلقة التي تستدعي التابع os.path.exists() للتحقق من وجود الملف ➌. سيؤدي العثور على أول اسم ملف غير موجود إلى كسر الحلقة باستخدام التعليمة break، لأنها عثرت على اسم الملف المضغوط الجديد. الخطوة الثانية: إنشاء ملف مضغوط ZIP جديد لننشئ الآن ملف ZIP، لذا اجعل برنامجك يبدو كما يلي: #! python3 # backupToZip.py - نسخ مجلد كامل ومحتوياته في ملف ZIP يزيد اسمه بمقدار واحد في كل مرة يُنسَخ فيها --snip-- while True: zipFilename = os.path.basename(folder) + '_' + str(number) + '.zip' if not os.path.exists(zipFilename): break number = number + 1 # إنشاء ملف مضغوط ZIP print(f'Creating {zipFilename}...') ➊ backupZip = zipfile.ZipFile(zipFilename, 'w') # TODO: المرور على شجرة المجلدات بأكملها وضغط الملفات الموجودة في كل مجلد print('Done.') backupToZip('C:\\delicious') خزّنا اسم ملف ZIP الجديد في المتغير zipFilename، ويمكننا الآن استدعاء الدالة zipfile.ZipFile() لإنشاء ملف ZIP فعليًا ➊. تأكد من تمرير 'w' كوسيطٍ ثانٍ لهذه الدالة لفتح الملف ZIP في وضع الكتابة. الخطوة الثالثة: المرور على شجرة المجلدات والإضافة إلى الملف المضغوط ZIP يجب الآن أن تستخدم الدالة os.walk() لسرد كل ملف موجود في المجلد ومجلداته الفرعية، لذا اجعل برنامجك يبدو كما يلي: #! python3 # backupToZip.py - نسخ مجلد كامل ومحتوياته في ملف ZIP يزيد اسمه بمقدار واحد في كل مرة يُنسَخ فيها --snip-- # المرور على شجرة المجلدات بأكملها وضغط الملفات الموجودة في كل مجلد ➊ for foldername, subfolders, filenames in os.walk(folder): print(f'Adding files in {foldername}...') # إضافة المجلد الحالي إلى ملف ZIP ➋ backupZip.write(foldername) # إضافة كافة الملفات الموجودة في هذا المجلد إلى ملف ZIP ➌ for filename in filenames: newBase = os.path.basename(folder) + '_' if filename.startswith(newBase) and filename.endswith('.zip'): continue # لا تنشئ نسخة احتياطية من ملفات ZIP الاحتياطية backupZip.write(os.path.join(foldername, filename)) backupZip.close() print('Done.') backupToZip('C:\\delicious') يمكنك استخدام الدالة os.walk() في حلقة for ➊، حيث ستعيد في كل تكرار اسم المجلد الحالي لهذا التكرار والمجلدات الفرعية الموجودة في هذا المجلد وأسماء الملفات الموجودة في هذا المجلد. يُضاف المجلد إلى ملف ZIP ➋ في حلقة for، ويمكن لحلقة for المتداخلة المرور على كل اسم ملف في القائمة filenames ➌، ويُضاف كل منها إلى ملف ZIP باستثناء ملفات ZIP الاحتياطية التي أنشأناها مسبقًا. سينتج ما يلي عند تشغيل هذا البرنامج: Creating delicious_1.zip... Adding files in C:\delicious... Adding files in C:\delicious\cats... Adding files in C:\delicious\waffles... Adding files in C:\delicious\walnut... Adding files in C:\delicious\walnut\waffles... Done. سيضع هذا البرنامج جميع الملفات الموجودة في المجلد C:\delicious في ملف ZIP بالاسم delicious_2.zip في المرة الثانية لتشغيله وهكذا. أفكار لبرامج مماثلة يمكنك الاستفادة من فكرة المرور على شجرة المجلدات وإضافة الملفات إلى ملفات الأرشفة ZIP المضغوطة في العديد من البرامج الأخرى، فمثلًا يمكنك كتابة البرامج التي تنجز المهام التالية: المرور على شجرة المجلدات وأرشفة الملفات التي لها امتدادات محددة فقط مثل .txt أو .py. المرور على شجرة المجلدات وأرشفة جميع الملفات باستثناء ملفات .txt و .py. البحث عن المجلد في شجرة المجلدات الذي يحتوي على أكبر عدد من الملفات أو المجلد الذي يستخدم أكبر مساحة على القرص الصلب. مشاريع للتدريب حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر. برنامج لإنشاء نسخة انتقائية للملفات من شجرة المجلدات اكتب برنامجًا يمر على شجرة المجلدات ويبحث عن الملفات التي لها امتداد ملف محدّد (مثل .pdf أو .jpg)، وانسخ هذه الملفات من أيّ موقع توجد فيه في مجلد جديد. برنامج لحذف الملفات غير الضرورية يمكن أن تَشغَل بعض الملفات أو المجلدات غير الضرورية والضخمة الجزء الأكبر من المساحة على قرص حاسوبك الصلب، فإذا أدرتَ تحرير مساحةٍ على حاسوبك، فستحصل على أقصى استفادة من خلال حذف أكبر عدد ممكن من الملفات غير المرغوب فيها، ولكن يجب أولًا العثور عليها. اكتب برنامجًا يمر على شجرة المجلدات ويبحث عن الملفات أو المجلدات الكبيرة استثنائيًا مثل الملفات أو المجلدات التي يزيد حجم ملفها عن 100 ميجابايت، حيث يمكنك استخدام الدالة os.path.getsize() من وحدة os للحصول على حجم الملف. اطبع هذه الملفات مع مسارها المطلق على الشاشة. ملء الفجوات في ترقيم أسماء الملفات اكتب برنامجًا يبحث عن جميع الملفات ذات البادئة المُحدَّدة -مثل spam001.txt و spam002.txt وإلخ- في مجلدٍ واحد، ويحدّد هذا البرنامج موقع أيّ فجوات في الترقيم مثل وجود الملفين spam001.txt و spam003.txt دون وجود الملف spam002.txt، لذا اجعل البرنامج يعيد تسمية جميع الملفات اللاحقة لسد هذه الفجوة. اكتب أيضًا برنامجًا آخر يمكنه إدراج فجوات في الملفات المرقّمة بحيث يمكن إضافة ملف جديد. الخلاصة يُحتمَل أنك تتعامل مع الملفات يدويًا باستخدام الفأرة ولوحة المفاتيح حتى لو كنت من مستخدمي الحاسوب ذوي الخبرة. تسهّل مستكشفات الملفات الحديثة العمل مع عددٍ من الملفات، ولكن ستحتاج في بعض الأحيان إلى تنفيذ مهمة قد تستغرق ساعات باستخدام مستكشف الملفات الخاص بحاسوبك. توفّر وحدتا os و shutil دوالًا لنسخ الملفات ونقلها وإعادة تسميتها وحذفها، ولكن قد ترغب في استخدام وحدة send2trash عند حذف الملفات لنقلها إلى سلة المحذوفات أو سلة المهملات بدلًا من حذفها نهائيًا. يُفضَّل تعليق الشيفرة البرمجية التي تنسخ أو تنقل أو تعيد التسمية أو تحذف الملفات فعليًا عند كتابة البرامج التي تتعامل مع الملفات، ويجب إضافة استدعاء الدالة print() حتى تتمكّن من تشغيل البرنامج والتحقق مما سيفعله بالضبط. يجب في أغلب الأحيان تنفيذ هذه العمليات على الملفات الموجودة في أحد المجلدات وعلى كل مجلد موجود في هذا المجلد وعلى كل مجلد موجود في تلك المجلدات وإلخ. تتولى الدالة os.walk() هذه الرحلة عبر المجلدات نيابةً عنك حتى تتمكّن من التركيز على ما يحتاج برنامجك إلى فعله مع الملفات الموجودة في هذه المجلدات. تمنحك وحدة zipfile طريقةً لضغط وفك ضغط الملفات في أرشيفات .ZIP باستخدام لغة بايثون. تسهّل الوحدة zipfile -مع دوال معالجة الملفات الخاصة بوحدتي os و shutil- تجميعَ العديد من الملفات من أيّ مكان على قرص حاسوبك الصلب. يُعَد رفع هذه الملفات المضغوطة ZIP على مواقع الويب أو إرسالها بوصفها مرفقات في البريد الإلكتروني أسهلَ بكثير من العديد من الملفات المنفصلة. وفرنا في هذه السلسلة من المقالات شيفرة برمجية يمكنك نسخها ولصقها في برنامجك، ولكن يُحتمَل ألّا تظهر بمظهرٍ مثالي في البداية. يركز المقال التالي على بعض وحدات بايثون التي ستساعدك على تحليل برامجك وتنقيح أخطائها لتتمكّن من تشغيلها بصورة صحيحة بسرعة. ترجمة -وبتصرُّف- للمقال Organizing Files لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: قراءة وكتابة الملفات باستخدام لغة بايثون python التعامل مع الملفات والمسارات في بايثون كيفية التعامل مع الملفات النصية في بايثون 3 مشاريع بايثون عملية تناسب المبتدئين
-
يناقش هذا المقال خاصيةً مهمةً أخرى للشعاع وهي خاصية الاتجاه، إذ ناقشنا في المقال السابق خاصية الطول، حيث تعرّف هاتان الخاصيتان الشعاع. سنوضّح في هذا المقال المواضيع التالية: المنحى Orientation أو اتجاه الحركة للأشعة ثنائية الأبعاد (مُمثَّلة في إطار إحداثي معين). الغموض عند استخدام قوس الظل arc tan لحساب الزاوية. تحويل الطول والمنحى إلى مكونات مصفوفة عمودية. قياس الزوايا بوحدتي الراديان Radian والدرجة المئوية. استخدام لغتي البرمجة سي C وجافا Java مع قوس الظل. الأشعة المحاذية للمحورين X-Y ما هو اتجاه الشعاع الذي تمثله المصفوفة العمودية (4, 0)T؟ يوازي هذا الشعاع المحور X لإطار الإحداثيات الذي نستخدمه، وسيكون هذا الشعاع غالبًا أفقيًا ويؤشّر إلى اليمين (أو يمكن القول أن الشعاع له منحى مقداره 0 درجة). للشعاع ثنائي الأبعاد الذي يحاذي محور الإحداثيات -كما هو الحال مع (4, 0)T- منحًى يسهل رؤيته، ويكون عند 0 درجة أو 90 درجة أو 180 درجة أو 270 درجة. يُعبَّر عن منحى الشعاع عادةً من خلال زاوية مع المحور x الموجب لإطار الإحداثيات، وهناك طريقتان لذلك: تُقاس الزاوية من 0 إلى 360 درجة بعكس اتجاه دوران عقارب الساعة ابتداءً من المحور x الموجب. تُقاس الزاوية من 0 إلى +180 درجة بعكس اتجاه دوران عقارب الساعة, ابتداءً من المحور x الموجب، أو تُقاس من 0 إلى -180 درجة مع اتجاه دوران عقارب الساعة ابتداءً من المحور x الموجب. حساب زاوية الشعاع عن المحور X ما هو اتجاه الشعاع a في المخطط البياني السابق؟ يكون منحى الشعاع a = (4, 4)T بزاوية مقدارها 45 درجة. يجب أن تعطي الرياضيات الإجابة نفسها، إذ يُقدَّر ميل Slope الشعاع a بما يلي: (التغير في قيمة x)/(التغير في قيمة y) = 4.0/4.0 = 1.0 بالتالي تكون الزاوية باستخدام قوس الظل هي: 45 = arc tan( 1.0 ) درجة. ليس للأشعة موقع، لذا لا يعتمد الطول ولا الاتجاه على المكان الذي ترسم فيه الشعاع، وتكون صيغة اتجاه الشعاع ثنائي الأبعاد هي: angle of (x, y)T = arc tan( y/x ) وللتأكيد يعني استخدام قوس الظل لقيمةٍ ما arc tan( z ) أنه تم العثور على الزاوية التي لها ظل tangent مقداره z، وتذكّر -عند حسابها باستخدام الآلة الحاسبة- أن الإجابة يمكن أن تكون بالراديان أو بالدرجات، إذ تمنحك معظم الآلات الحاسبة خيار استخدام أيّ من هذين التنسيقين. وبما أن الآلات الحاسبة تعطي عادةً الإجابة محصورةً بين -90.0 و +90.0 درجة (أو بين -pi و +pi راديان)، فقد تضطر إلى تعديل الإجابة (سنوضح ذلك لاحقًا). يمكنك أيضًا استخدام الآلة الحاسبة على حاسوبك، فإذا كنت تستخدم نظام التشغيل ويندوز، فانقر على الخيار "علمي Scientific" في قائمة "العرض View" للآلة الحاسبة التي تحتوي على قوس الظل arc tan الذي يمكن أن يكون له تسمية هي tan-1. ما هو منحى الشعاع الذي تمثله المصفوفة العمودية: k = (3,4)T (باستخدام الآلة الحاسبة)؟ arc tan( y/x ) = arc tan( 4/3 ) = arc tan( 1.333333333333 ) = 53.13 درجة التعامل مع القيم السالبة يوضح المخطط البياني السابق الشعاع الذي تمثله المصفوفة العمودية: k = (3,4)T، والذي حسبنا منحاه ليكون 53.13 درجةً عن محور x الموجب، وبالتالي يبدو كل شيء صحيحًا. لنحسب الآن منحى الشعاع -k = (-3,-4)T من خلال استخدام الصيغة التالية: arc tan( y/x ) = arc tan( -4/-3 ) = arc tan( 4/3 ) = arc tan( 1.333333333333 ) = 53.13 درجة لاحظ وجود خطأ، إذ أعطتنا هذه الصيغة الزاوية نفسها للشعاع الذي يؤشّر إلى الاتجاه المعاكس لاتجاه الشعاع الأول. تكمن المشكلة في فقدان المعلومات عند قسمة -4 على -3، إذ لا يمكننا تفريق هذه النتيجة عن نتيجة قسمة +4 على +3، وبالتالي هذه الصيغة ليست كافيةً لتعطيك الإجابة الصحيحة، ويجب عليك رسم الشعاع ثم تعديل الإجابة. لاحظ من المخطط البياني أن منحى الشعاع -k الذي يُعبَّر عنه بالدرجات من 0 إلى 360 بعكس اتجاه عقارب الساعة عن المحور x هو (180 + 53.13) = 233.13 درجة. ما هو منحى الشعاع الذي تمثله المصفوفة العمودية: p = (3,-4)T(استخدم تطبيق الآلة الحاسبة الموجود على حاسوبك)؟ ضع الأعداد في حاسبة ويندوز كما يلي: arc tan( y/x ) = arc tan( -4/3 ) = arc tan( -1.333333333333 ) = -53.13 درجة يبدو الحساب صحيحًا في المخطط البياني السابق، وإذا أردتَ التعبير عن الزاوية بعكس اتجاه دوران عقارب الساعة، فهي 360 - 53.130 = 306.870 درجة، ولكن سيعطيك الشعاع -p النتيجة نفسها للأسف، لذا ارسم دائمًا الشعاع عندما تحسب منحاه، بعدها يمكنك أن ترى أن الزاوية التي تريدها للشعاع -p هي 180 - 53.130 = 126.87 درجة. تتوفر الدالة atan2(y, x) في معظم لغات البرمجة، إذ تحسب هذه الدالة الزاوية بالراديان بين المحور x الموجب والنقطة المعطاة بالإحداثيات (x, y)، وتستخدِم إشارتَي x و y لتحديد الربع الصحيح للزاوية. ما هو منحى الشعاع u = (-4,-2)T ؟ (ارسم الشعاع أولًا، ثم استخدم الآلة الحاسبة)؟ عوّض القيم في الصيغة واستخدم الآلة الحاسبة لإيجاد الناتج: arc tan( y/x ) = arc tan( -2/-4 ) = arc tan( 0.5 ) = 26.565 درجة ولكن سترى عند رسم الشعاع أنه يقع في الربع الخطأ، لذا يجب أن تكون الإجابة 180 + 26.565 = 206.565 درجة. حساب السمتين (x, y) الخاصتين بالشعاع لنفترض أنك تعلم أن الشعاع يمكن تمثيله بمصفوفة عمودية (x, y)T باستخدام إطار إحداثي معين، ويمكن حساب هاتين السمتين x و y كما يلي: orientation of (x, y)T = arc tan( y/x ) |(x, y)T| = √( x2 + y2 ) ارسم الشعاع r الذي تمثله المصفوفة العمودية (4, 5)T، ثم قدّر طوله واتجاهه، وأجرِ العمليات الحسابية للحصول على الإجابة الدقيقة كما يلي: الطول: | r | = | (4,5)T | = √(16+25) = 6.40 الاتجاه: direction = arc tan{ 5/4 } = 51.34 درجة تحويل الطول والاتجاه إلى السمتين (x, y) لنفترض أن لديك طول واتجاه وتريد التعبير عنهما بشعاع عمودي ثنائي الأبعاد، يمكنك فعل ذلك باتباع الخطوات التالية: أنشئ مخططًا بيانيًا. احسب قيمة x من خلال إسقاط الطول على المحور x باستخدام length*cos( θ ). احسب قيمة y من خلال إسقاط الطول على المحور y باستخدام length*sin( θ ). تحقق من الإجابات وفق المخطط البياني. لنفترض أن لدينا شعاعًا طوله 4 ومنحاه بزاويةٍ مقدارها 150 درجة. إذًا ما هي المصفوفة العمودية التي تمثّل هذا الشعاع؟ سنمثّل هذا الشعاع بالمصفوفة العمودية ثنائية الأبعاد ( -3.464, 2.0 )T . حساب الزوايا بالراديان اتبع الخطوات التالية في هذا الحساب: أنشئ مخططًا بيانيًا كما يلي: احسب قيمة x من خلال إسقاط الطول على المحور x كما يلي: 4 * cos( 150 ) = -3.464 احسب قيمة y من خلال إسقاط الطول على المحور y كما يلي: 4 * sin( 150 ) = 2.0 تحقق من الإجابات وفق المخطط البياني (تبدو جيدة). كن حذرًا بشأن استخدام هذه الصيغ وتوقّع الإجابات الصحيحة، خاصةً عند البرمجة بلغة سي أو جافا، إذ يمكن للمكتبات الرياضية الخاصة بلغة البرمجة أن تعطي أشياءً غير متوقعة إن لم تكن حذرًا، ولكن توجد ثلاثة أماكن يجب توخي الحذر فيها بصورة خاصة وهي: من المتوقع أن يكون وسيط sin() و cos() و tan() بالراديان، والقيمة التي تعيدها atan() بالراديان. من المتوقع أن يكون وسيط معظم الدوال الرياضية من النوع double، ولكن إذا استخدمتَ عددًا عشريًا float أو عددًا صحيحًا int، فلن تحصل على رسالة خطأ، بل مجرد إجابة غريبة غير صحيحة. توجد عدة إصدارات من دالة "arc tan" في معظم مكتبات لغة سي، وكلٌ منها مخصَّص لمجال مختلف من قيم الخرج. يُعبَّر عن الزوايا عادةً بالراديان، وتُقاس الزوايا بعكس اتجاه عقارب الساعة ابتداءً من المحور x الموجب (أو تُقاس الزاوية السالبة باتجاه عقارب الساعة ابتداءً من المحور x الموجب أحيانًا)، إذ يوجد ما مقداره 2 pi راديان في الدائرة الكاملة، أي أن: ((2 pi راديان = 360 درجة لنفترض أن لدينا شعاعًا له طول length وزاوية angle! أوجد في هذه الحالة قيمة vector[0] التي تمثل المركّبة x، وأوجد قيمة vector[1] التي تمثّل المركّبة y الخاصة بهذا الشعاع. #include <math.h> double length, angle; /* طول وزاوية الشعاع */ double vector[2]; /* عناصر الشعاع */ . . . length = قيمةٌ ما angle = عددٌ ما بالدرجات vector[0] = ???? vector[1] = ???? لنفترض أن رمز pi هو M_PI، ولكن لسوء الحظ تستخدم المصرّفات Compilers المختلفة رموز PI مختلفة وتعرّفها في ترويسات ملفات مختلفة، لذا يجب أن تستخدم الرمز المناسب لنظام تشغيلك بدلًا من تعريف PI بنفسك. ملاحظة: إذا لم تكن على دراية بلغة سي، فيمكنك عَدّ هذه الشيفرة بأنها مكتوبة بلغة جافا. #include <math.h> double length, angle; double vector[2]; . . . length = قيمةٌ ما angle = عددٌ ما بالدرجات vector[0] = length * cos( angle*M_PI/180.0 ) vector[1] = length * sin( angle*M_PI/180.0 ) تدريب عملي تذكّر أولًا الخطوات التي يجب اتخاذها لتحويل الشعاع المعلوم طوله واتجاهه إلى (x,y): أنشئ مخططًا بيانيًا. احسب قيمة x من خلال إسقاط الطول على المحور x باستخدام length*cos( θ ). احسب قيمة y من خلال إسقاط الطول على المحور y باستخدام length*sin( θ ). 4: تحقق من الإجابات وفق المخطط البياني. وتبقى الخطوات نفسها إذا كانت الزاوية مُعطاةً بالراديان. تدريب 1: ليكن لدينا شعاع طوله 4.5 (بأيّ وحدة طول) وزاوية منحاه 0.70 راديان، ولنعبّر عن هذا الشعاع بالصيغة ( x, y )T كما يلي: أنشئ مخططًا بيانيًا كما يلي: احسب قيمة x من خلال إسقاط الطول على المحور x كما يلي: 4.5*cos( 0.70 ) = 3.442 احسب قيمة y بإسقاط الطول على المحور y كما يلي: 4.5*sin( 0.70 ) = 2.899 تحقق من الإجابات وفق المخطط البياني. تدريب 2: لنفترض أن الدعسوقة ليلى أضاعت صديقتها سلوى، ولكن كان مع ليلى هاتف محمول لحسن الحظ وكان: موقع ليلى عند النقطة (1, -4). موقع سلوى عند النقطة ( -4, 3). اتصل بليلى وأخبرها في أيّ اتجاه وإلى أيّ مسافة يجب أن تمشي للوصول إلى صديقتها سلوى. الحل: الاتجاه = 125.54 درجة، والمسافة = 8.60. كان عليك أن تتذكر كيفية حساب الإزاحة للحصول على الإجابة، إذ يمكنك حساب شعاع الإزاحة بين نقطتين (أو بين الدعسوقتين) كما يلي: (المكان الذي تريد أن تكون فيه) - (مكانك الحالي) = (الإزاحة التي تحتاجها) إذًا الإزاحة التي يجب أن تمشيها ليلى هي: ( -4, 3) - (1, -4) = (-5, 7) ولكن تحتاج ليلى إلى مسافة وجهة هما: المسافة: √(25 + 49) = 8.60 الاتجاه: arc tan( 7/-5 ) = arc tan( -1.4 ) = -54.46 درجة نعدّل الاتجاه بالنظر إلى المخطط البياني ويصبح الاتجاه هو: 180 - 54.46 = 125.54 درجة هل يمكن تطبيق الأفكار المتعلقة بمنحى الشعاع ثنائي الأبعاد الواردة في هذا المقال في الفضاء ثلاثي الأبعاد؟ نعم، ولكن ليس بهذه السهولة، إذ سيتطلب منحى الأشعة ثلاثية الأبعاد مزيدًا من العمل. وصلنا إلى نهاية هذا المقال الذي تعرّفنا فيه على خاصية الاتجاه الخاصة بالشعاع، وسنناقش في المقال التالي حاصل ضرب شعاع وقيمة عددية. ترجمة -وبتصرُّف- للفصل Vector Direction من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق تعرف على خاصية الطول Length للأشعة الهندسية وكيفية حسابه. المعاملات (parameters) في جافا. الأنواع الحقيقية والصحيحة في لغة سي C.
-
يوضح هذا المقال كيفية تعريف صفحة لحذف كائن المؤلف وتحديث كائن الكتاب للتعرّف على المزيد حول الاستمارات Forms في إطار عمل Express. استمارة حذف مؤلف سنوضح كيفية تعريف صفحة لحذف كائنات المؤلف Author، إذ ستكون استراتيجيتنا -كما ناقشنا في قسم تصميم الاستمارة- هي السماح فقط بحذف الكائنات التي لا تشير إليها كائنات أخرى، وهذا يعني أننا لن نسمح بحذف المؤلف Author إذا أشار إليه كتاب Book، ويعني ذلك من حيث التقديم أن الاستمارة يجب أن تؤكد عدم وجود كتب مرتبطة بالمؤلف قبل حذفه. tإذا كان هناك كتب مرتبطة به، فيجب أن تعرضها وتوضح أنه يجب حذفها قبل حذف كائن المؤلف Author. متحكم وجهة Get افتح الملف /controllers/authorController.js، ثم ابحث عن تابع المتحكم author_delete_get() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض استمارة حذف مؤلف في طلب GET exports.author_delete_get = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل المؤلف وجميع كتبه على التوازي const [author, allBooksByAuthor] = await Promise.all([ Author.findById(req.params.id).exec(), Book.find({ author: req.params.id }, "title summary").exec(), ]); if (author === null) { // لا توجد نتائج res.redirect("/catalog/authors"); } res.render("author_delete", { title: "Delete Author", author: author, author_books: allBooksByAuthor, }); }); يحصل المتحكم على معرّف نسخة المؤلف Author لحذفه من معامل عنوان URL (وهو req.params.id)، ويستخدم await لانتظار الوعد الذي أعاده التابع Promise.all() للانتظار بطريقة غير متزامنة لسجل المؤلف المُحدَّد وجميع الكتب المرتبطة به على التوازي، ويصيّر العرض author_delete.pug عند اكتمال كلتا العمليتين، مع تمرير متغيرات title و author و author_books. ملاحظة: إن لم يُعِد التابع findById() أيّ نتائج، فلن يكون المؤلف موجودًا في قاعدة البيانات، وبالتالي لا يوجد شيء يمكن حذفه، لذا نعيد التوجيه مباشرةً إلى قائمة جميع المؤلفين. if (author === null) { // لا توجد نتائج res.redirect("/catalog/authors"); } متحكم طلب وجهة Post ابحث عن تابع المتحكم author_delete_post() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // معالجة حذف مؤلف في طلب POST exports.author_delete_post = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل المؤلف وجميع كتبه على التوازي const [author, allBooksByAuthor] = await Promise.all([ Author.findById(req.params.id).exec(), Book.find({ author: req.params.id }, "title summary").exec(), ]); if (allBooksByAuthor.length > 0) { // المؤلف لديه كتب، لذا اعرض باستخدام طريقة وجهة GET نفسها res.render("author_delete", { title: "Delete Author", author: author, author_books: allBooksByAuthor, }); return; } else { // المؤلف ليس لديه كتب، لذا احذف الكائن وأعِد التوجيه إلى قائمة المؤلفين await Author.findByIdAndRemove(req.body.authorid); res.redirect("/catalog/authors"); } }); نتحقق الكود أولًا من تقديم المعرّف المُرسَل عبر معاملات متن الاستمارة بدلًا من استخدام النسخة الموجودة في عنوان URL، ثم يحصل على المؤلف والكتب المرتبطة به باستخدام طريقة وِجهة GET نفسها. إن لم يكن هناك كتب، فإننا نحذف كائن المؤلف ونعيد التوجيه إلى قائمة جميع المؤلفين، وإذا كان هناك كتب، فسنعيد عرض الاستمارة مع تمرير المؤلف وقائمة الكتب لحذفها. ملاحظة: يمكننا التحقق من أن استدعاء التابع findById() يعيد أيّ نتائج، فإن لم يكن الأمر كذلك، نعرض قائمة جميع المؤلفين مباشرةً. لقد تركنا الشيفرة كما هي موضحة أعلاه للإيجاز، إذ ستظل تعيد قائمة المؤلفين إن لم يُعثَر على المعرّف، ولكن ذلك سيحدث بعد التابع findByIdAndRemove(). العرض View أنشئ العرض /views/author_delete.pug وضع فيه النص التالي: extends layout block content h1 #{title}: #{author.name} p= author.lifespan if author_books.length p #[strong Delete the following books before attempting to delete this author.] div(style='margin-left:20px;margin-top:20px') h4 Books dl each book in author_books dt a(href=book.url) #{book.title} dd #{book.summary} else p Do you really want to delete this Author? form(method='POST' action='') div.form-group input#authorid.form-control(type='hidden',name='authorid', required='true', value=author._id ) button.btn.btn-primary(type='submit') Delete يوسّع هذا العرض قالب التخطيط Layout من خلال تعديل الكتلة content، إذ يعرض تفاصيل المؤلف في أعلى الملف، ثم يتضمن تعليمة شرطية تعتمد على عدد كتب المؤلف author_books (تعليمات if و else) كما يلي: إذا كان هناك كتب مرتبطة بالمؤلف، فستسرد الصفحة الكتب وتشير إلى أنه يجب حذفها قبل حذف هذا المؤلف Author. إن لم يكن هناك كتب، فستعرض الصفحة طلبًا لتأكيد الحذف. إذا نُقِر على زر الحذف Delete، فسيُرسَل معرّف المؤلف إلى الخادم في طلب POST وسيُحذَف سجل المؤلف. إضافة عنصر تحكم الحذف Delete Control سنضيف الآن عنصر تحكم الحذف إلى عرض تفاصيل المؤلف، إذ تُعَد صفحة التفاصيل مكانًا جيدًا لحذف سجل منه. ملاحظة: سيكون عنصر التحكم مرئيًا فقط للمستخدمين المُصرَّح لهم عند التقديم الكامل، ولكن ليس لدينا نظام تصريح حاليًا. افتح العرض author_detail.pug وأضِف الأسطر التالية إلى نهايته: hr p a(href=author.url+'/delete') Delete author يجب أن يظهر عنصر التحكم الآن بوصفه رابطًا في صفحة تفاصيل المؤلف كما يلي: كيف تبدو استمارة حذف المؤلف؟ شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدّد رابط جميع المؤلفين All authors، واختر مؤلفًا معينًا، ثم حدّد رابط حذف المؤلف Delete author. إن لم يكن لدى المؤلف كتب، فستُعرَض صفحة مثل الصفحة التالية، إذ سيحذف الخادم المؤلف ويعيد التوجيه إلى قائمة المؤلفين بعد الضغط على زر الحذف: إذا كان لدى المؤلف كتب، فسيُقدَّم عرض يشبه ما يلي، ثم يمكنك حذف الكتب من صفحات تفاصيلها (بعد تقديم الشيفرة البرمجية المتعلقة بذلك): ملاحظة: يمكن تقديم الصفحات الأخرى الخاصة بحذف الكائنات باستخدام الطريقة نفسها، لذا تركناها كتحدٍ لك. استمارة تحديث كتاب سنوضح كيفية تعريف صفحة لتحديث كائنات الكتاب Book، إذ تشبه معالجة استمارة تحديث كتاب إلى حدٍ كبير معالجة استمارة إنشاء كتاب، باستثناء أنه يجب عليك ملء الاستمارة في وِجهة GET بقيمٍ من قاعدة البيانات. متحكم وجهة Get افتح الملف "/controllers/bookController.js"، ثم ابحث عن تابع المتحكم book_update_get() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض استمارة تحديث كتاب في طلب GET exports.book_update_get = asyncHandler(async (req, res, next) => { // الحصول على الكتاب والمؤلفين وأنواع الكتب للاستمارة const [book, allAuthors, allGenres] = await Promise.all([ Book.findById(req.params.id).populate("author").populate("genre").exec(), Author.find().sort({ family_name: 1 }).exec(), Genre.find().sort({ name: 1 }).exec(), ]); if (book === null) { // لا توجد نتائج const err = new Error("Book not found"); err.status = 404; return next(err); } // ميّز أنواع الكتب المختارة بوصفّها مُحدَّدة for (const genre of allGenres) { for (const book_g of book.genre) { if (genre._id.toString() === book_g._id.toString()) { genre.checked = "true"; } } } res.render("book_form", { title: "Update Book", authors: allAuthors, genres: allGenres, book: book, }); }); يحصل المتحكم على معرّف الكتاب Book لتحديثه من معامل عنوان URL (هو req.params.id)، وينتظر (باستخدام await) الوعد الذي أعاده التابع Promise.all() للحصول على سجل الكتاب Book المحدد (مع ملء حقلي نوع الكتاب والمؤلف) وجميع سجلات المؤلف Author ونوع الكتاب Genre. تتحقق الدالة عند اكتمال العمليات من العثور على أيّ كتب، فإن لم يُعثَر على أيٍّ منها، فسترسل خطأ عدم العثور على الكتاب "Book not found" إلى البرمجية الوسيطة لمعالجة الخطأ. ملاحظة: لا يُعَد عدم العثور على أيّ كتاب كنتيجة خطأً في البحث، ولكنه كذلك في هذا التطبيق لأننا نعلم أنه يجب أن يكون هناك سجل كتاب مطابق. تطبّق الشيفرة السابقة اختبار المقارنة (book===null) في دالة رد النداء، ولكن يمكن أن تضيف التابع orFail() إلى السلسلة التعاقبية الخاصة بالاستعلام. نميّز بعد ذلك أنواع الكتب المختارة حاليًا بوصفها مُحدَّدة، ثم نقدّم العرض book_form.pug، ونمرر متغيرات title والكتاب وجميع المؤلفين authors وجميع أنواع الكتب genres. متحكم وجهة Post ابحث عن تابع المتحكم book_update_post() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // معالجة تحديث كتاب في طلب POST exports.book_update_post = [ // تحويل نوع الكتاب إلى مصفوفة (req, res, next) => { if (!Array.isArray(req.body.genre)) { req.body.genre = typeof req.body.genre === "undefined" ? [] : [req.body.genre]; } next(); }, // التحقق من صحة الحقول وتطهيرها body("title", "Title must not be empty.") .trim() .isLength({ min: 1 }) .escape(), body("author", "Author must not be empty.") .trim() .isLength({ min: 1 }) .escape(), body("summary", "Summary must not be empty.") .trim() .isLength({ min: 1 }) .escape(), body("isbn", "ISBN must not be empty").trim().isLength({ min: 1 }).escape(), body("genre.*").escape(), // طلب العملية بعد التحقق من صحة البيانات وتطهيرها asyncHandler(async (req, res, next) => { // استخراج أخطاء التحقق من صحة البيانات من الطلب const errors = validationResult(req); // إنشاء كائن كتاب مع بيانات مُهرَّبة أو محذوف منها المسافات والمعرّف القديم const book = new Book({ title: req.body.title, author: req.body.author, summary: req.body.summary, isbn: req.body.isbn, genre: typeof req.body.genre === "undefined" ? [] : req.body.genre, _id: req.params.id, // هذا مطلوب، أو سيجري إسناد معرّف جديد }); if (!errors.isEmpty()) { // توجد أخطاء، لذا اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة أو رسائل خطأ // الحصول على جميع المؤلفين وأنواع الكتب للاستمارة const [allAuthors, allGenres] = await Promise.all([ Author.find().exec(), Genre.find().exec(), ]); // ميّز أنواع الكتب المختارة بوصفها مُحدَّدة for (const genre of allGenres) { if (book.genre.indexOf(genre._id) > -1) { genre.checked = "true"; } } res.render("book_form", { title: "Update Book", authors: allAuthors, genres: allGenres, book: book, errors: errors.array(), }); return; } else { // البيانات الواردة من الاستمارة صالحة، لذا حدّث السجل const thebook = await Book.findByIdAndUpdate(req.params.id, book, {}); // أعِد التوجيه إلى صفحة تفاصيل الكتاب res.redirect(thebook.url); } }), ]; تُعَد هذه الشيفرة البرمجية مشابهة جدًا لشيفرة وِجهة Post التي استخدمناها لإنشاء كتاب Book، إذ نتحقق أولًا من صحة بيانات الكتاب القادمة من الاستمارة ونطهرها ونستخدمها لإنشاء كائن كتاب Book جديد مع ضبط قيمة _id الخاصة به على معرّف الكائن المُراد تحديثه. إذا كان هناك أخطاء عند التحقق من صحة البيانات، فسنعيد تصيير الاستمارة، بالإضافة إلى عرض البيانات التي أدخلها المستخدم والأخطاء وقوائم أنواع الكتب والمؤلفين. إذا لم يكن هناك أخطاء، فسنستدعي التابع Book.findByIdAndUpdate() لتحديث مستند الكتاب Book، ثم نعيد التوجيه إلى صفحة تفاصيله. العرض View ليست هناك حاجة لتغيير عرض الاستمارة (/views/book_form.pug)، إذ يناسب هذا القالب إنشاء الكتاب وتحديثه. إضافة زر تحديث افتح العرض book_detail.pug وتأكّد من وجود روابط لكل من حذف وتحديث الكتب أسفل الصفحة كما يلي: hr p a(href=book.url+'/delete') Delete Book p a(href=book.url+'/update') Update Book يجب أن تكون قادرًا الآن على تحديث الكتب من صفحة تفاصيل الكتاب. كيف تبدو استمارة تحديث الكتاب؟ شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدّد رابط جميع الكتب All books، واختر كتابًا معينًا، ثم حدّد رابط تحديث الكتاب Update Book. يجب أن تبدو الاستمارة تمامًا مثل صفحة إنشاء كتاب Create book، ولكن مع العنوان "Update book"، وتكون الاستمارة مملوءة مسبقًا بقيم السجل. ملاحظة: يمكن تقديم الصفحات الأخرى الخاصة بتحديث الكائنات باستخدام الطريقة نفسها، لذا تركناها كتحدٍ لك. ترجمة -وبتصرُّف- للمقالين Delete Author form و Update Book form. اقرأ المزيد المقال السابق إنشاء مكتبة محلية باستخدام Express: إضافة الكتب والمؤلفين. مدخل إلى إطار عمل الويب Express وبيئة Node.js إعداد بيئة تطوير Node مع Express توثيق Node.js باللغة العربية إطار عمل Express
-
يوضح هذا المقال كيفية تعريف صفحات لإنشاء كائنات المؤلف والكتاب ونسخ الكتب للتعرّف أكثر على كيفية التعامل مع الاستمارات forms في إطار عمل Express. استمارة إنشاء مؤلف سنوضّح فيما يلي كيفية تعريف صفحة لإنشاء كائنات المؤلف Author. استيراد توابع التحقق من صحة البيانات Validation وتطهيرها Sanitization يجب طلب الدوال التي نريدها لاستخدام express-validator كما هو الحال في استمارة نوع الكتاب. افتح الملف "/controllers/authorController.js"، وأضِف السطر التالي في بداية الملف قبل دوال الوِجهة Route: const { body, validationResult } = require("express-validator"); متحكم وجهة Get ابحث عن تابع المتحكم author_create_get() المُصدَّر وضع مكانه الشيفرة البرمجية التالية التي تؤدي إلى تقديم العرض author_form.pug وتمرير متغير العنوان title: // عرض استمارة إنشاء مؤلف في طلب GET exports.author_create_get = (req, res, next) => { res.render("author_form", { title: "Create Author" }); }; متحكم وجهة Post ابحث عن تابع المتحكم author_create_post() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // معالجة إنشاء مؤلف Author في طلب POST exports.author_create_post = [ // التحقق من صحة الحقول وتطهيرها body("first_name") .trim() .isLength({ min: 1 }) .escape() .withMessage("First name must be specified.") .isAlphanumeric() .withMessage("First name has non-alphanumeric characters."), body("family_name") .trim() .isLength({ min: 1 }) .escape() .withMessage("Family name must be specified.") .isAlphanumeric() .withMessage("Family name has non-alphanumeric characters."), body("date_of_birth", "Invalid date of birth") .optional({ values: "falsy" }) .isISO8601() .toDate(), body("date_of_death", "Invalid date of death") .optional({ values: "falsy" }) .isISO8601() .toDate(), // طلب العملية بعد التحقق من صحة البيانات وتطهيرها asyncHandler(async (req, res, next) => { // استخراج أخطاء التحقق من صحة البيانات من الطلب const errors = validationResult(req); // إنشاء كائن مؤلف مع بيانات مُهرَّبة ومحذوف منها المسافات const author = new Author({ first_name: req.body.first_name, family_name: req.body.family_name, date_of_birth: req.body.date_of_birth, date_of_death: req.body.date_of_death, }); if (!errors.isEmpty()) { // توجد أخطاء، لذا اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة أو رسائل خطأ res.render("author_form", { title: "Create Author", author: author, errors: errors.array(), }); return; } else { // البيانات الواردة من الاستمارة صالحة // احفظ المؤلف await author.save(); // أعِد التوجيه إلى سجل مؤلف جديد res.redirect(author.url); } }), ]; تحذير: لا تتحقق من صحة الأسماء باستخدام isAlphanumeric() كما فعلنا، نظرًا لوجود العديد من الأسماء التي تستخدم مجموعات محارف أخرى، ولكننا فعلنا ذلك لشرح كيفية استخدام أداة التحقق من صحة البيانات Validator، وكيفية ربطها بسلسلة تعاقبية مع أدوات التحقق الأخرى والإبلاغ عن الأخطاء. تماثل بنية وسلوك هذه الشيفرة البرمجية تقريبًا شيفرة إنشاء كائن نوع الكتاب Genre، إذ نتحقق أولًا من صحة البيانات ونطهرها، فإذا كانت البيانات غير صالحة سنعيد عرض الاستمارة مع البيانات التي أدخلها المستخدم في الأصل وقائمة برسائل الخطأ. وإذا كانت البيانات صالحة، سنحفظ سجل المؤلف الجديد ونعيد توجيه المستخدم إلى صفحة تفاصيل المؤلف. لا نتحقق مما إذا كان كائن المؤلف Author موجودًا مسبقًا قبل حفظه، وهذا يختلف عن معالج طلب Genre من النوع Post، إذ يمكننا ذلك بالرغم من أنه يمكن أن يكون لدينا عدة مؤلفين بالاسم نفسه. توضح شيفرة التحقق من صحة البيانات العديد من الميزات الجديدة التالية: أولًا، يمكننا إنشاء سلسلة تعاقبية من أدوات التحقق باستخدام withMessage() لتحديد رسالة الخطأ التي ستُعرَض في حالة فشل تابع التحقق السابق، مما يسهّل تقديم رسائل خطأ محددة دون الكثير من الشيفرة البرمجية المكررة. [ // التحقق من صحة الحقول وتطهيرها body("first_name") .trim() .isLength({ min: 1 }) .escape() .withMessage("First name must be specified.") .isAlphanumeric() .withMessage("First name has non-alphanumeric characters."), // … ]; ثانيًا، يمكننا استخدام الدالة optional() لإجراء عملية تحقق لاحقة فقط في حالة الإدخال في حقلٍ ما، مما يسمح بالتحقق من صحة الحقول الاختيارية، فمثلًا نتحقق فيما يلي من أن تاريخ الميلاد الاختياري هو تاريخ متوافق مع معيار ISO8601، إذ يعني تمرير كائن {values: "falsy"} أننا سنقبل إما سلسلة نصية فارغة أو null للقيمة الفارغة. [ body("date_of_birth", "Invalid date of birth") .optional({ values: "falsy" }) .isISO8601() .toDate(), ]; ثالثًا، تُستلَم المعاملات من الطلب بوصفها سلاسلًا نصية، ويمكننا استخدام toDate() أو toBoolean() لتغيير هذه الأنواع إلى أنواع جافا سكريبت المناسبة كما هو موضح في نهاية سلسلة أدوات التحقق سابقًا. العرض View أنشئ العرض "/views/author_form.pug" وضع فيه النص التالي: extends layout block content h1=title form(method='POST' action='') div.form-group label(for='first_name') First Name: input#first_name.form-control(type='text' placeholder='First name' name='first_name' required='true' value=(undefined===author ? '' : author.first_name) ) label(for='family_name') Family Name: input#family_name.form-control(type='text' placeholder='Family name' name='family_name' required='true' value=(undefined===author ? '' : author.family_name)) div.form-group label(for='date_of_birth') Date of birth: input#date_of_birth.form-control(type='date' name='date_of_birth' value=(undefined===author ? '' : author.date_of_birth) ) button.btn.btn-primary(type='submit') Submit if errors ul for error in errors li!= error.msg تماثل بنية وسلوك هذا العرض ما هو موجود في قالب genre_form.pug، لذلك لن نشرحه مرةً أخرى. ملاحظة: لا تدعم بعض المتصفحات حقل الإدخال type="date"، لذلك لن تحصل على عنصر واجهة مستخدم منتقي التاريخ أو العنصر البديل الافتراضي dd/mm/yyyy، ولكن ستحصل بدلًا من ذلك على حقل نص عادي فارغ. يتمثل أحد الحلول في إضافة السمة placeholder='dd/mm/yyyy' صراحةً، بحيث تظل تحصل على معلومات حول تنسيق النص المطلوب في المتصفحات ذات القدرات الأقل. التحدي: إضافة تاريخ الوفاة يفتقد هذا القالب حقلًا لإدخال تاريخ الوفاة date_of_death، لذا أنشئ هذا الحقل باتباع النمط نفسه لمجموعة استمارة تاريخ الميلاد. كيف تبدو استمارة المؤلف؟ شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدّد رابط إنشاء مؤلف جديد Create new author. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ القيمة بعد إدخالها وستُنقَل إلى صفحة تفاصيل المؤلف. ملاحظة: إذا جرّبتَ تنسيقات إدخال مختلفة للتواريخ، فقد تجد أن التنسيق yyyy-mm-dd لا يتصرف بصورة صحيحة، لأن جافا سكريبت تتعامل مع سلاسل التاريخ بأنها تتضمن وقت 0 ساعة، ولكنها تتعامل أيضًا مع سلاسل التاريخ بهذا التنسيق (معيار ISO 8601) بوصفها تتضمن الوقت 0 ساعة بالتوقيت العالمي المنسَّق UTC بدلًا من التوقيت المحلي. إذا وقعت منطقتك الزمنية غرب توقيت UTC، فسيكون عرض التاريخ -لكونه محليًا- قبل يوم واحد من التاريخ الذي أدخلته، وهذه إحدى التعقيدات العديدة، مثل أسماء العائلات متعددة الكلمات والكتب متعددة المؤلفين التي لم نعالجها. استمارة إنشاء كتاب سنوضح كيفية تعريف صفحة أو استمارة لإنشاء كائنات الكتاب Book، ويُعَد ذلك أكثر تعقيدًا قليلًا من صفحات المؤلف Author أو نوع الكتاب Genre المكافئة لها، لأننا نحتاج إلى الحصول على سجلات المؤلف Author ونوع الكتاب Genre المتوفرة وعرضها في استمارة الكتاب Book. استيراد توابع التحقق من صحة البيانات وتطهيرها افتح الملف /controllers/bookController.js، وأضِف السطر التالي في بداية الملف قبل دوال الوِجهة Route: const { body, validationResult } = require("express-validator"); متحكم وجهة Get ابحث عن تابع المتحكم book_create_get() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض استمارة إنشاء كتاب في طلب GET exports.book_create_get = asyncHandler(async (req, res, next) => { // الحصول على جميع المؤلفين وأنواع الكتب التي يمكننا استخدامها للإضافة إلى الكتاب const [allAuthors, allGenres] = await Promise.all([ Author.find().sort({ family_name: 1 }).exec(), Genre.find().sort({ name: 1 }).exec(), ]); res.render("book_form", { title: "Create Book", authors: allAuthors, genres: allGenres, }); }); نستخدم await لانتظار نتيجة التابع Promise.all() للحصول على جميع كائنات المؤلف Author ونوع الكتاب Genre على التوازي، وهو الأسلوب نفسه الذي استخدمناه عند عرض بيانات المكتبة، ثم تُمرَّر إلى العرض book_form.pug بوصفها متغيرات بالاسم authors و genres مع عنوان title الصفحة. متحكم وجهة Post ابحث عن تابع المتحكم book_create_post() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // معالجة إنشاء كتاب في طلب POST exports.book_create_post = [ // تحويل نوع الكتاب إلى مصفوفة (req, res, next) => { if (!Array.isArray(req.body.genre)) { req.body.genre = typeof req.body.genre === "undefined" ? [] : [req.body.genre]; } next(); }, // التحقق من صحة الحقول وتطهيرها body("title", "Title must not be empty.") .trim() .isLength({ min: 1 }) .escape(), body("author", "Author must not be empty.") .trim() .isLength({ min: 1 }) .escape(), body("summary", "Summary must not be empty.") .trim() .isLength({ min: 1 }) .escape(), body("isbn", "ISBN must not be empty").trim().isLength({ min: 1 }).escape(), body("genre.*").escape(), // طلب العملية بعد التحقق من صحة البيانات وتطهيرها asyncHandler(async (req, res, next) => { // استخراج أخطاء التحقق من صحة البيانات من الطلب const errors = validationResult(req); // إنشاء كائن كتاب مع بيانات مُهرَّبة ومحذوف منها المسافات const book = new Book({ title: req.body.title, author: req.body.author, summary: req.body.summary, isbn: req.body.isbn, genre: req.body.genre, }); if (!errors.isEmpty()) { // توجد أخطاء، لذا اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة أو رسائل خطأ // احصل على جميع المؤلفين وأنواع الكتب للاستمارة const [allAuthors, allGenres] = await Promise.all([ Author.find().sort({ family_name: 1 }).exec(), Genre.find().sort({ name: 1 }).exec(), ]); // ميّز أنواع الكتب المختارة بوصفها محددة for (const genre of allGenres) { if (book.genre.includes(genre._id)) { genre.checked = "true"; } } res.render("book_form", { title: "Create Book", authors: allAuthors, genres: allGenres, book: book, errors: errors.array(), }); } else { // البيانات الواردة من الاستمارة صالحة، لذا احفظ الكتاب await book.save(); res.redirect(book.url); } }), ]; تماثل بنية وسلوك هذه الشيفرة البرمجية تقريبًا دوال وجهة Post لاستمارات النوع Genre والكاتب Author، إذ نتحقق أولًا من صحة البيانات ونطهّرها؛ فإذا كانت البيانات غير صالحة، فسنعيد عرض الاستمارة مع البيانات التي أدخلها المستخدم في الأصل وقائمة برسائل الخطأ؛ وإذا كانت البيانات صالحة، فسنحفظ سجل الكتاب Book الجديد ونعيد توجيه المستخدم إلى صفحة تفاصيل الكتاب. يتمثل الاختلاف الرئيسي فيما يتعلق بشيفرة معالجة الاستمارة الأخرى في كيفية تطهير معلومات نوع الكتاب، إذ تعيد الاستمارة مصفوفةً من عناصر Genre، بينما تعيد سلسلة نصية بالنسبة للحقول الأخرى. نحوّل أولًا الطلب إلى مصفوفة (مطلوبة للخطوة التالية) للتحقق من صحة المعلومات. [ // تحويل نوع الكتاب إلى مصفوفة (req, res, next) => { if (!Array.isArray(req.body.genre)) { req.body.genre = typeof req.body.genre === "undefined" ? [] : [req.body.genre]; } next(); }, // … ]; نستخدم بعد ذلك محرف بدل (*) في أداة التطهير للتحقق من صحة كل إدخال من مصفوفة أنواع الكتاب بصورة فردية، إذ توضح الشيفرة التالية كيفية ترجمة ذلك إلى "تطهير كل عنصر له المفتاح genre". [ // … body("genre.*").escape(), // … ]; الاختلاف الأخير فيما يتعلق بشيفرة معالجة الاستمارة الأخرى هو أنه نحتاج إلى تمرير جميع أنواع الكتب والمؤلفين الموجودين إلى الاستمارة. نكرر عبر جميع أنواع الكتب ونضيف المعامل checked="true" إلى تلك الأنواع التي كانت موجودة في بيانات Post (كما أعدنا الإنتاج في جزء الشيفرة التالي) لتمييز أنواع الكتب التي حدّدها المستخدم. // تمييز أنواع الكتب المختارة بوصفها مُحدَّدة for (const genre of results.genres) { if (book.genre.includes(genre._id)) { // اختير نوع الكتاب الحالي، لذا اضبط الراية "checked" genre.checked = "true"; } } العرض أنشئ العرض "/views/book_form.pug" وضع فيه النص التالي: extends layout block content h1= title form(method='POST' action='') div.form-group label(for='title') Title: input#title.form-control(type='text', placeholder='Name of book' name='title' required='true' value=(undefined===book ? '' : book.title) ) div.form-group label(for='author') Author: select#author.form-control(type='select', placeholder='Select author' name='author' required='true' ) for author in authors if book option(value=author._id selected=(author._id.toString()===book.author._id.toString() ? 'selected' : false) ) #{author.name} else option(value=author._id) #{author.name} div.form-group label(for='summary') Summary: textarea#summary.form-control(type='textarea', placeholder='Summary' name='summary' required='true') #{undefined===book ? '' : book.summary} div.form-group label(for='isbn') ISBN: input#isbn.form-control(type='text', placeholder='ISBN13' name='isbn' value=(undefined===book ? '' : book.isbn) required='true') div.form-group label Genre: div for genre in genres div(style='display: inline; padding-right:10px;') input.checkbox-input(type='checkbox', name='genre', id=genre._id, value=genre._id, checked=genre.checked ) label(for=genre._id) #{genre.name} button.btn.btn-primary(type='submit') Submit if errors ul for error in errors li!= error.msg تماثل بنية وسلوك هذا العرض تقريبًا بنية وسلوك القالب genre_form.pug، ولكن تكمن الاختلافات الرئيسية في كيفية تقديم حقول نوع الاختيار: Author و Genre، إذ: تُعرَض مجموعة أنواع الكتب بوصفها مربعات اختيار، وتستخدم قيمة checked التي ضبطناها في المتحكم لتحديد ما إذا كان يجب تحديد المربع أم لا. تُعرَض مجموعة المؤلفين بوصفها قائمة منسدلة مع اختيار فردي مرتبة أبجديًا، فإذا حدّد المستخدم مسبقًا مؤلف كتابٍ ما عند إصلاح قيم الحقول غير الصالحة بعد إرسال الاستمارة الأولية أو عند تحديث تفاصيل الكتاب مثلًا، فسيُعاد اختيار المؤلف عند عرض الاستمارة. نحدد في هذه الحالة المؤلف المراد اختياره من خلال موازنة معرّف خيار المؤلف الحالي مع القيمة التي أدخلها المستخدم مسبقًا والمُمرَّرة عبر المتغير book. ملاحظة: إذا كان هناك خطأ في الاستمارة المُرسَلة، فسيكون معرّف مؤلف الكتاب الجديد ومعرّفات مؤلفي الكتب الموجودين مسبقًا من النوع Schema.Types.ObjectId عند إعادة تصيير الاستمارة، لذا يجب تحويلها إلى سلاسل نصية أولًا للموازنة بينها. كيف تبدو استمارة إنشاء كتاب؟ شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط إنشاء كتاب جديد Create new book. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ الكتاب بعد إرسال كتاب صالح وستُنقَل إلى صفحة تفاصيل الكتاب: استمارة إنشاء نسخة كتاب BookInstance سنوضح فيما يلي كيفية تعريف صفحة أو استمارة لإنشاء كائنات BookInstance، وتشبه هذه الاستمارة إلى حد كبير الاستمارة التي استخدمناها لإنشاء كائنات الكتاب Book. استيراد توابع التحقق من صحة البيانات وتطهيرها افتح الملف "/controllers/bookinstanceController.js"، وضِف السطر التالي في بداية الملف: const { body, validationResult } = require("express-validator"); متحكم وجهة Get اطلب الوحدة Book في الجزء العلوي من الملف، حيث تُعَد هذه الوحدة مطلوبة لأن كل نسخة كتاب BookInstance مرتبطة بكتاب Book معين. const Book = require("../models/book"); ابحث عن تابع المتحكم bookinstance_create_get() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض استمارة إنشاء نسخة كتاب BookInstance في طلب GET exports.bookinstance_create_get = asyncHandler(async (req, res, next) => { const allBooks = await Book.find({}, "title").sort({ title: 1 }).exec(); res.render("bookinstance_form", { title: "Create BookInstance", book_list: allBooks, }); }); يحصل المتحكم على قائمة بجميع الكتب (allBooks) ويمرّرها عبر المتغير book_list إلى العرض bookinstance_form.pug مع المتغير title. لاحظ عدم تحديد أي كتاب عندما أظهرنا الاستمارة لأول مرة، وبالتالي لم يُمرّر المتغير selected_book إلى دالة التصيير ()render، ونتيجةً لذلك سيكون للمتغير selected_book قيمة undefined في القالب. متحكم وجهة Post ابحث عن تابع المتحكم bookinstance_create_post() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // معالجة إنشاء نسخة كتاب في طلب POST exports.bookinstance_create_post = [ // التحقق من صحة الحقول وتطهيرها body("book", "Book must be specified").trim().isLength({ min: 1 }).escape(), body("imprint", "Imprint must be specified") .trim() .isLength({ min: 1 }) .escape(), body("status").escape(), body("due_back", "Invalid date") .optional({ values: "falsy" }) .isISO8601() .toDate(), // طلب العملية بعد التحقق من صحة البيانات وتطهيرها asyncHandler(async (req, res, next) => { // استخراج أخطاء التحقق من صحة البيانات من الطلب const errors = validationResult(req); // إنشاء كائن نسخة كتاب BookInstance مع بيانات مُهرَّبة ومحذوف منها المسافات const bookInstance = new BookInstance({ book: req.body.book, imprint: req.body.imprint, status: req.body.status, due_back: req.body.due_back, }); if (!errors.isEmpty()) { // توجد أخطاء // اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة ورسائل خطأ const allBooks = await Book.find({}, "title").sort({ title: 1 }).exec(); res.render("bookinstance_form", { title: "Create BookInstance", book_list: allBooks, selected_book: bookInstance.book._id, errors: errors.array(), bookinstance: bookInstance, }); return; } else { // البيانات الواردة من الاستمارة صالحة await bookInstance.save(); res.redirect(bookInstance.url); } }), ]; تماثل بنية وسلوك هذه الشيفرة البرمجية شيفرة إنشاء الكائنات الأخرى، إذ نتحقق أولًا من صحة البيانات ونطهرها؛ فإذا كانت البيانات غير صالحة، فسنعيد عرض الاستمارة مع البيانات التي أدخلها المستخدم في الأصل وقائمة برسائل الخطأ؛ وإذا كانت البيانات صالحة، فسنحفظ سجل نسخة الكتاب BookInstance الجديد ونعيد توجيه المستخدم إلى صفحة التفاصيل. العرض أنشئ العرض "/views/bookinstance_form.pug" وضع فيه النص التالي: extends layout block content h1=title form(method='POST' action='') div.form-group label(for='book') Book: select#book.form-control(type='select' placeholder='Select book' name='book' required='true') for book in book_list option(value=book._id, selected=(selected_book==book._id.toString() ? '' : false) ) #{book.title} div.form-group label(for='imprint') Imprint: input#imprint.form-control(type='text' placeholder='Publisher and date information' name='imprint' required='true' value=(undefined===bookinstance ? '' : bookinstance.imprint)) div.form-group label(for='due_back') Date when book available: input#due_back.form-control(type='date' name='due_back' value=(undefined===bookinstance ? '' : bookinstance.due_back_yyyy_mm_dd)) div.form-group label(for='status') Status: select#status.form-control(type='select' placeholder='Select status' name='status' required='true' ) option(value='Maintenance' selected=(undefined===bookinstance || bookinstance.status!='Maintenance' ? false:'selected')) Maintenance option(value='Available' selected=(undefined===bookinstance || bookinstance.status!='Available' ? false:'selected')) Available option(value='Loaned' selected=(undefined===bookinstance || bookinstance.status!='Loaned' ? false:'selected')) Loaned option(value='Reserved' selected=(undefined===bookinstance || bookinstance.status!='Reserved' ? false:'selected')) Reserved button.btn.btn-primary(type='submit') Submit if errors ul for error in errors li!= error.msg تماثل بنية وسلوك هذا العرض تقريبًا بنية وسلوك القالب book_form.pug، لذا لن نشرحها مرةً أخرى. ملاحظة: يجعل القالب السابق قيم الحالة Status ثابتة (في الصيانة Maintenance ومتوفر Available وغير ذلك)، ولا يتذكر القيم التي أدخلها المستخدم، فإذا رغبتَ في ذلك، ففكر في إعادة تقديم القائمة وتمرير بيانات الخيار من المتحكم وضبط القيمة المختارة عند إعادة عرض الاستمارة. كيف تبدو استمارة إنشاء نسخة كتاب؟ شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط إنشاء نسخة كتاب جديدة Create new book instance (copy). إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ نسخة الكتاب بعد إرسال نسخة كتاب صالحة وستُنقَل إلى صفحة التفاصيل: ترجمة -وبتصرُّف- للمقالات Create Author form و Create Book form و Create BookInstance form. اقرأ أيضًا المقال السابق: إنشاء مكتبة محلية باستخدام Express: إنشاء استمارة Form لأنواع الكتب الزلات البرمجية والأخطاء في جافاسكريبت تعلم لغة جافا سكريبت JavaScript مدخل إلى إطار عمل الويب Express وبيئة Node
-
يناقش هذا المقال طول Length الأشعة وكيفية حساب هذا الطول باستخدام تمثيل المصفوفة العمودية للأشعة، وسنناقش في المقال التالي خاصيةً أخرى للأشعة، وهي خاصية الاتجاه Direction؛ إذ للأشعة بجميع أبعادها خاصيتان هما: الطول والاتجاه، ولكن سنستخدم أشعة في فضاء ثنائي الأبعاد للسهولة. سنوضّح في هذا المقال المواضيع التالية: طول الأشعة ثنائية وثلاثية الأبعاد. صيغة فيثاغورس. حساب طول الشعاع من خلال تمثيله بالمصفوفة العمودية. طول الشعاع الصفري. طول الشعاع السالب. الأشعة المحاذية للمحورين X-Y ما هو طول الشعاع التالي الذي تمثله المصفوفة العمودية (3, 0)T؟ يمكن القول أن الطول يساوي 3 وحدات. إذا كان هناك شعاع ثنائي الأبعاد يحاذي المحور x، فإن المصفوفة العمودية التي تمثله لها قيمة غير صفرية في العنصر الأول، وصفر في العنصر الثاني، إذ من السهل تحديد طول الشعاع الأخضر المُمثَّل بالمصفوفة العمودية (3, 0)Tفي المخطط البياني التالي، فالطول هو قيمة العنصر الوحيد غير الصفري في تلك المصفوفة. يحاذي الشعاع a (الشعاع الأزرق في المخطط البياني السابق) المحور y، ومصفوفته هي (0, 4)T، فما هو طول الشعاع a؟ طوله 4 وحدات، إذ يمكن قراءته من المخطط البياني، ولكن ليس للشعاع موقع ثابت طبعًا، لذلك يمكن رسم الشعاع a في أيّ مكان وسيكون له الطول نفسه. صيغة فيثاغورس يوضح المخطط البياني التالي شعاعين بطول 3 و4 مع الشعاع الجديد h: من الصعب معرفة طول الشعاع h، ولكنه ليس صعبًا كثيرًا؛ خاصةً إذا كنت على معرفة بقاعدة "3، 4، 5 في المثلثات قائمة الزاوية"، إذ يمكن ترتيب الأشعة الثلاثة في مثلث قائم الزاوية، بحيث يكون h هو الوتر، والضلعان الآخران هما 3 و 4؛ وبالتالي فإن طول h يساوي 5 باستخدام صيغة فيثاغورس والتي هي: (طول الوتر)2 = (طول الضلع القائمة الأولى)2 + (طول الضلع القائمة الثانية)2 إذا استخدمنا صيغة فيثاغورس مع مثلث قائم الزاوية له ضلعان قائمان طولهما 3 و 4، فسنحصل على طول الوتر كما يلي: (طول الوتر)2 = 32 + 42 (طول الوتر)2 = 9 + 16 = 25 (طول الوتر) = 5 إذًا ما هو طول الوتر في المثلث القائم الزاوية الذي طول ضلعيه 6 و 8؟ وما طول الوتر في المثلث القائم الزاوية الذي طول ضلعيه القائمين 6 و 8؟ سنستخدم للحل صيغة فيثاغورس كما يلي: الطول2 = 62 + 62 الطول2 = 36 + 64 = 100 الطول = 10 استخدام صيغة فيثاغورس لحساب طول الأشعة يمكن استخدام صيغة فيثاغورس لحساب طول الشعاع ثنائي الأبعاد. لنفترض أن الشعاع تمثّله المصفوفة العمودية (x, y)T، لذا ضع ذيل الشعاع عند نقطة الأصل، ثم أنشئ مثلثًا من خلال رسم الضلعين الآخرين كما في المخطط البياني السابق: side_1 = (x, 0)T side_2 = (0, y)T طول الضلع الأول side_1 هو x، وطول الضلع الثاني side_2 هو y، وبالتالي ينتج لدينا أن الطول: length (x, y)T = √( xT + yT ) يمثّل الرمز √ الجذرَ التربيعي الموجب في هذه الصيغة، إذ لا نريد بالطبع أن يكون الطول سالبًا. ما طول الشعاع الذي تمثله المصفوفة العمودية (4, 3)T؟ الطول يساوي 5.0، إذ يمكنك استخدام الصيغة السابقة، أو إدراك أنه مثلث قائم زاوية آخر أطول أضلاعه 3-4-5. الرمز المستخدم لطول الشعاع تعمل صيغة فيثاغورس مع الأشعة المحاذية للمحاور كما يلي: length of( (8,0)T ) = √( 8*8 + 0*0 ) = 8 لا يُعَد الاستمرار في قول "طول ( ) أو length of ( )" مناسبًا، لذا يوجد رمزٌ لذلك وهو: طول | a | = ( a ) للرمز a شريطٌ عمودي من كل جانب، وتستخدم الكتب أحيانًا شريطين عموديين على كل جانب، وبالتالي تكون الصيغة كما يلي: | (x, y)T | = √( x2 + y2 ) إذا كان g = (1, 1)T، فما هو | g | ؟ نعوّض في الصيغة كما يلي: | g | = √( 12 + 12 ) = √2.0 = 1.414213562373 لاحظ أن الطول ليس 1.0 ولا 2.0، وهذا خطأ يقع فيه بعض الأشخاص. إثبات متراجحة المثلث تذكّر أن طول مجموع شعاعين أقل من أو يساوي مجموع طولي هذين الشعاعين: طول ( v + u ) <= طول ( u ) + طول ( v ) وفي الصيغة الجديدة: | u + v | <= | u | + | v | لتكن لدينا الأشعة المُمثَّلة بالمصفوفات العمودية التالية: u = (3, 4)T v = (3, -4)T w = u + v = (6, 0)T يوضّح المخطط البياني التالي كيفية عمل متراجحة المثلث في هذا المثال، إذ يكون مجموع طولي الشعاعين الأحمر والأزرق أقل من طول الشعاع الأخضر. ولكن هل يمكن تطبيق ذلك عدديًا؟ وما هي قيمة كلٍّ من | u | و | v | و | w |؟ إجابة: u = (3, 4)T وبالتالي: | u | = 5 v = (3, -4)T وبالتالي: | v | = 5 w = u + v = (6, 0)T وبالتالي: | w | = 6 طبعًا 6 < 5 + 5 مما يدل على أن: | u + v | <= | u | + | v | تدريب عملي لا تكون عناصر الشعاع عادةً قيمًا صحيحةً بسيطة، ولتكن لدينا مثلًا بعض الأشعة المُمثَّلة بمصفوفات عمودية تحتوي على كسور عشرية في عناصرها كما يلي: q = (2.2, 3.6)T r = (-4.8, -2.2)T s = q + r يمكن طبعًا استخدام صيغة فيثاغورس، ولكنك ستحتاج إلى آلة حاسبة لاستخدامها. | (x, y)T | = √( x2 + y2 ) ما هي أطوال الأشعة q و r و s؟ | q | = √( 2.22.2 + 3.63.6 ) = √( 4.84 + 12.96 ) = √ 17.8 = 4.219 | r | = √( -4.8* -4.8 + -2.2 * -2.2 ) = √( 23.04 + 4.84 ) = √ 27.88 = 5.280 | s | = √( -2.6 * -2.6 + 1.4*1.4 ) = √( 6.76 + 1.96 ) = √ 8.72 = 2.953 يكون | s | أصغر من | q | + | r | كما هو متوقع. حساب طول الأشعة ثلاثية الأبعاد تكون للأشعة ثلاثية الأبعاد خاصية الطول، ويمكن استخدام الصيغة فيثاغورس نفسها لحساب الطول، إذ يكون طول الشعاع الذي تمثله مصفوفة مكونة من ثلاثة مكونات هو: | (x, y, z)T | = √( x2 + y2 + z2) كما في المثال التالي: | (1, 2, 3)T | = √( 12 + 22 + 32 ) = √( 1 + 4 + 9 ) = √14 = 3.742 إذًا لنحسب طول (2, -4, 4)T و (-1, -2, 3)T كما يلي: | (2, -4, 4)T | = √( 2 * 2 + -4 * -4 + 4 * 4) = √( 4+ 16 + 16 ) = √36 = 6 | (-1, -2, 3)T | = √( -1 * -1 + -2 * -2 + 3 * 3) = √( 1 + 4 + 9 ) = √14 = 3.742 يعطي تربيع عناصر الشعاع ناتج جمع للقيم الموجبة (أو الصفرية)، مما يضمن أن يكون الطول قيمة موجبة (أو صفرًا). الأشعة الهندسية ضع في بالك أن الأشعة هي كائنات هندسية، أي يُعبَّر عنها بطولٍ واتجاه في الفضاء. تُمثَّل الأشعة بالمصفوفات العمودية، وتفترض صيغ حساب الطول في هذا المقال استخدام إطار إحداثي وأن الأشعة مُمثَّلة بمصفوفات عمودية ضمن هذا الإطار. يمكن أن يناقش أيّ كتابٍ خاص بالرسوميات الحاسوبية كيفية استخدام الإحداثيات المتجانسة Homogeneous Coordinates لتمثيل الأشعة، إذ تستخدم هذه الطريقة مصفوفات عمودية مكونة من 4 مكونات لتمثيل الأشعة في فضاء ثلاثي الأبعاد، ويتطلب حساب طول الشعاع المُمثَّل بهذه الطريقة تعديل الصيغ، مع تجاهل المكون الرابع للمصفوفة العمودية. لا تقلق بشأن هذه الطريقة حاليًا، إذ سنوضّح التفاصيل لاحقًا، وخذ الوقت الكافي لفهم فكرة أن المصفوفات العمودية التي نستخدمها ليست الطريقة الوحيدة لتمثيل الأشعة، وأن خاصية الطول هي خاصة بالشعاع، وليست خاصة بالمصفوفة العمودية التي تمثل هذا الشعاع، لذا قد تتساءل عن وجود أيّ أهمية لتمييز شيء ما عن تمثيله في علوم الحاسوب! حسنًا، نعم هناك أهمية طبعًا، ويُعَد ذلك من أهم الأفكار في علوم الحاسوب. ليكن لدينًا مثلًا المخطط البياني التالي الذي يحتوي شعاعًا وإطارين للإحداثيات هما: إطار رمادي فاتح وإطار برتقالي. نمثّل الشعاع بالمصفوفة العمودية (8, 6)T في الإطار الرمادي الفاتح، ونمثّله في الإطار البرتقالي بالمصفوفة العمودية (9.8, 2)T، ولنفترض أن المحاور في كلا الإطارين مُدرَّجة باستخدام الوحدات نفسها (سم مثلًا). يكون طول الشعاع باستخدام التمثيل الأول هو: √( 82 + 62 ) = √( 64 + 36 )= √( 100 )= 10.0 ويكون طول الشعاع باستخدام التمثيل الثاني هو: √( 9.82 + 22 ) = √( 96 + 4 ) = √( 100 ) = 10.0 ولكن هل ستنجح العملية الحسابية إذا وضعنا ذيل الشعاع في مكان آخر من الإطار؟ نعم، فليس للشعاع موضع محدّد، لذلك يمكن وضع الذيل في أيّ مكان تريده. الطول قيمة موجبة أو صفرية بما أن مربع الطول يساوي مجموع المربعات، وينتج عن تربيع الأعداد الحقيقية قيمة صفرية أو موجبة، فهذا يعني أنه يجب أن يكون الطول دائمًا صفرًا أو عددًا موجبًا. | a | = | (a1, a2, a3 )T | = √( a12 + a22 + a32 ) >= 0 يكون طول الشعاع ثلاثي الأبعاد صفرًا فقط عندما يكون هذا الشعاع هو الشعاع الصفري، إذ يُمثَّل الشعاع الصفري ثلاثي الأبعاد في جميع الإطارات الإحداثية كما يلي: 0 = (0, 0, 0)T ويكون طول الشعاع الصفري هو: | 0 | = √( 02 + 02 + 02 ) = 0 يكون طول الشعاع الصفري ثنائي الأبعاد صفرًا أيضًا، وهو الشعاع ثنائي الأبعاد الوحيد الذي يكون طوله صفرًا. إذًا هل سيكون للشعاعين المتساويين الطول نفسه، وهل سيكون الشعاعان اللذان لهما الطول نفسه متساويين دائمًا؟ سيكون للشعاعين المتساويين الطول نفسه، إذ يجب أن تكون عناصرهما المتقابلة متساويةً، لذا يجب أن تكون عمليات التربيع المتقابلة متساوية أيضًا، وبالتالي يجب أن يكون المجموع متساويًا، ويجب أن يكون الطول متساويًا. لن يكون الشعاعان اللذان لهما الطول نفسه متساويين دائمًا، إذ يمكن أن يكون مجموع مربعات العناصر متساويًا دون أن تكون العناصر نفسها متساوية. طول الشعاع السالب إذا كان v شعاعًا، فإن -v هو شعاع يؤشّر إلى الاتجاه المعاكس، وإذا كان تمثيل v هو (a, b, c)T، فسيكون تمثيل -v هو (-a, -b, -c)T، فما هي العلاقة بين طول الشعاعين v وطول -v؟ العلاقة هي: | v | = |-v| الأشعة ذات الاتجاهات المتعاكسة يوضّح المخطط البياني التالي بيانيًا شعاعين باتجاهين متعاكسين في فضاء ثنائي الأبعاد: يمكنك تمثيل المخطط البياني السابق رياضيًا كما يلي: | v | = |(a, b, c)T| = √( a2 + b2 + c2) |-v | = |(-a, -b, -c)T| = √( -a2 + -b2 + -c2) = √( a2 + b2 + c2 ) يمكن القول أن الشعاعين لهما الطول نفسه، ولكنهما يؤشّران إلى اتجاهين متعاكسين كما سنوضّح في المقال التالي. يمكن حساب طول الشعاع الذي تمثله المصفوفة العمودية (1, -1, 1 )T كما يلي: √( 12 + -12 + 12) = √3 ترجمة -وبتصرُّف- للفصل Vector Length من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: كيفية جمع الأشعة الهندسية للتصاميم ثلاثية الأبعاد كيفية جمع وطرح المصفوفات العمودية والمصفوفات السطرية الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد التعرف على النقاط والخطوط في الرسوميات الحاسوبية ثلاثية الأبعاد
-
يوضح هذا المقال كيفية تعريف صفحة لإنشاء كائنات النوع Genre، إذ يُعَد ذلك مكانًا جيدًا لتعلم التعامل مع الاستمارات Forms لأن الاستمارة الخاصة بنوع الكتاب Genre تحتوي على حقل واحد فقط هو اسم النوع name وبدون اعتماديات، ويجب إعداد الوجهات Routes والمتحكمات Controllers والعروض Views لهذه الصفحة مثلها مثل أيّ صفحة أخرى. استيراد توابع التحقق من صحة البيانات Validation وتطهيرها Sanitization يمكن استخدام express-validator في المتحكمات من خلال طلب require الدوال التي نريد استخدامها للتحقق من صحة حقول النماذج من الوحدة 'express-validator'. افتح الملف "/controllers/genreController.js"، وأضف السطر التالي في بداية الملف قبل أيٍّ من دوال معالج الوجهة: const { body, validationResult } = require("express-validator"); ملاحظة: تسمح الصيغة السابقة باستخدام body و validationResult بوصفهما دوال وسيطة كما سترى في قسم طلب الوجهة من النوع Post، وهي تكافئ ما يلي: const validator = require("express-validator"); const body = validator.body; const validationResult = validator.validationResult; متحكم وجهة Get ابحث عن تابع المتحكم genre_create_get() المُصدَّر وضع مكانه الشيفرة البرمجية التالية التي تؤدي إلى تقديم التصيير genre_form.pug وتمرير متغير العنوان: // عرض استمارة إنشاء نوع كتاب في طلب GET exports.genre_create_get = (req, res, next) => { res.render("genre_form", { title: "Create Genre" }); }; لاحظ أن ذلك يؤدي إلى وضع دالة معالج وجهة Express "العادي" مكان المعالج غير المتزامن للعنصر البديل الذي أضفناه في مقال الوجهات والمتحكمات. لا حاجة للمغلّف asyncHandler() لهذه الوجهة، لأنه لا يحتوي على أيّ شيفرة برمجية يمكنها رمي استثناء. متحكم وجهة Post ابحث عن تابع المتحكم genre_create_post() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // معالجة إنشاء نوع كتاب Genre في طلب POST exports.genre_create_post = [ // التحقق من صحة حقل الاسم وتطهيره body("name", "Genre name must contain at least 3 characters") .trim() .isLength({ min: 3 }) .escape(), // طلب العملية بعد التحقق من صحة البيانات وتطهيرها asyncHandler(async (req, res, next) => { // استخراج أخطاء التحقق من صحة البيانات من الطلب const errors = validationResult(req); // إنشاء كائن نوع كتاب مع بيانات مُهرَّبة ومحذوف منها المسافات const genre = new Genre({ name: req.body.name }); if (!errors.isEmpty()) { // توجد أخطاء، لذا اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة أو رسائل خطأ res.render("genre_form", { title: "Create Genre", genre: genre, errors: errors.array(), }); return; } else { // البيانات الواردة من الاستمارة صالحة // تحقق مما إذا كان نوع الكتاب الذي يحمل الاسم نفسه موجود مسبقًا const genreExists = await Genre.findOne({ name: req.body.name }).exec(); if (genreExists) { // نوع الكتاب موجود مسبقًا، لذا أعِد التوجيه إلى صفحة تفاصيل هذا النوع res.redirect(genreExists.url); } else { await genre.save(); // حُفِظ نوع الكتاب الجديد، لذا أعِد التوجيه إلى صفحة تفاصيل نوع الكتاب res.redirect(genre.url); } } }), ]; أول شيء يجب ملاحظته هو أن المتحكم يحدّد مصفوفة من الدوال الوسيطة بدلًا من وجود دالة وسيطة واحدة مع الوسائط (req, res, next)، وتُمرَّر المصفوفة إلى دالة الموجِّه ويُستدعَى كل تابع حسب ترتيبه. ملاحظة: يُعَد هذا الأسلوب ضروريًا، لأن أدوات التحقق من صحة البيانات Validators هي دوال وسيطة. يعرِّف التابع الأول في المصفوفة أداة التحقق من صحة الجسم (body()) التي تتحقق من صحة الحقل وتطهّره، وتستخدم الدالةtrim() لإزالة أي مسافة بيضاء لاحقة أو بادئة، وتتحقق من أن حقل الاسم name غير فارغ، ثم تستخدم الدالة escape() لإزالة أيّ محارف HTML خطيرة. [ // تحقق من أن حقل الاسم غير فارغ body("name", "Genre name must contain at least 3 characters") .trim() .isLength({ min: 3 }) .escape(), // … ]; بعد تحديد أدوات التحقق من الصحة، ننشئ دالة وسيطة لاستخراج أيّ أخطاء في عملية التحقق من صحة البيانات، إذ نستخدم التابع isEmpty() للتحقق ما إذا كان هناك أيّ أخطاء في نتيجة التحقق من صحة البيانات، وإذا كان هناك أخطاء، فسنصيّر الاستمارة مرةً أخرى، ونمرّر كائن نوع الكتاب المُطهَّر ومصفوفة رسائل الخطأ (errors.array()). // طلب العملية بعد التحقق من صحة البيانات وتطهيرها asyncHandler(async (req, res, next) => { // استخراج أخطاء التحقق من صحة البيانات من الطلب const errors = validationResult(req); // إنشاء كائن نوع كتاب مع بيانات مُهرَّبة ومحذوف منها المسافات const genre = new Genre({ name: req.body.name }); if (!errors.isEmpty()) { // توجد أخطاء، لذا اعرض الاستمارة مرة أخرى مع قيم مُطهَّرة أو رسائل خطأ res.render("genre_form", { title: "Create Genre", genre: genre, errors: errors.array(), }); return; } else { // البيانات الواردة من الاستمارة صالحة // … } }); إذا كانت بيانات اسم نوع الكتاب صالحة، فسنتحقق من وجود نوع كتاب Genre يحمل الاسم نفسه، لأننا لا نريد إنشاء نسخ مكررة. إذا كان الأمر كذلك، فسنعيد التوجيه إلى صفحة تفاصيل نوع الكتاب الموجود مسبقًا، وإن لم يكن الأمر كذلك، فسنحفظ نوع الكتاب Genre الجديد ونعيد التوجيه إلى صفحة تفاصيله. لاحظ أننا ننتظر باستخدام await نتيجة استعلام قاعدة البيانات باتباع النمط نفسه لمعالجات الوِجهة الأخرى. // تحقق مما إذا كان نوع الكتاب الذي يحمل الاسم نفسه موجودًا مسبقًا const genreExists = await Genre.findOne({ name: req.body.name }).exec(); if (genreExists) { // نوع الكتاب موجود مسبقًا، لذا أعِد التوجيه إلى صفحة تفاصيله res.redirect(genreExists.url); } else { await genre.save(); // حُفِظ نوع الكتاب الجديد، لذا أعِد التوجيه إلى صفحة تفاصيل نوع الكتاب res.redirect(genre.url); } سنستخدم هذا النمط في جميع المتحكمات اللاحقة، إذ سنشغّل أدوات التحقق من صحة البيانات (مع أدوات التطهير)، ثم نتحقق من الأخطاء وإما نعيد عرض الاستمارة مع معلومات الخطأ أو نحفظ البيانات. العرض View يُصيّر العرض نفسه في كل من متحكمات أو وِجهات GET و POST عندما ننشئ نوع كتاب Genre جديد (وسيُستخدَم لاحقًا عند تحديث نوع كتاب Genre) ، ولكن في حالة GET تكون الاستمارة فارغة ونمرر متغير العنوان فقط، بينما في حالة POST قد يكون المستخدم قد أدخل بيانات غير صالحة سابقًا ، لذا نمرّر في المتغير genre نسخة مُطهَّرة من البيانات المُدخَلة ونمرّر مصفوفة من رسائل الخطأ في المتغير errors. توضح الشيفرة التالية شيفرة المتحكم لتصيير القالب في كلتا الحالتين: // تصيير وِجهة GET res.render("genre_form", { title: "Create Genre" }); // تصيير وِجهة POST res.render("genre_form", { title: "Create Genre", genre, errors: errors.array(), }); أنشئ العرض "/views/genre_form.pug" وضع فيه النص التالي: extends layout block content h1 #{title} form(method='POST' action='') div.form-group label(for='name') Genre: input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' required='true' value=(undefined===genre ? '' : genre.name) ) button.btn.btn-primary(type='submit') Submit if errors ul for error in errors li!= error.msg يُعَد أغلب هذا القالب مألوفًا من مقال سابق، إذ سنوسّع أولًا القالب الأساسي layout.pug ونعدّل الكتلة block التي اسمها 'content'، ثم لدينا عنوان مع المتغير title الذي مرّرناه من المتحكم عبر التابع render(). لدينا بعد ذلك شيفرة Pug الخاصة باستمارة HTML التي تستخدم method="POST" لإرسال البيانات إلى الخادم، وستُرسَل البيانات إلى عنوان URL نفسه الخاص بالصفحة لأن الإجراء action هو سلسلة نصية فارغة. تعرِّف الاستمارة حقلًا واحدًا مطلوبًا من النوع النصي "text" اسمه "name"يمثل اسم نوع الكتاب، وتعتمد القيمة الافتراضية للحقل على ما إذا كان المتغير genre مُعرَّفًا أم لا. إذا اُستدعِي هذا الحقل من وِجهة GET، فسيكون فارغًا لأنها استمارة جديدة، وإذا اُستدعِي من وِجهة POST، فسيحتوي على القيمة غير الصالحة التي أدخلها المستخدم في الأصل. الجزء الأخير من الصفحة هو شيفرة الخطأ التي تؤدي إلى طباعة قائمة الأخطاء، إذا كان متغير الخطأ مُعرَّفًا، وبالتالي لن يظهر هذا القسم عند عرض القالب في وجهة GET. ملاحظة: عرضنا طريقة واحدة فقط لتصيير الأخطاء، ويمكنك أيضًا الحصول على أسماء الحقول المتأثرة من متغير الخطأ، واستخدامها للتحكم في مكان عرض رسائل الخطأ، وما إذا أدرتَ تطبيق شيفرة CSS مخصصة وغير ذلك. كيف تبدو استمارة نوع الكتاب؟ شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدّد رابط إنشاء نوع كتاب جديد Create new genre. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي، ويجب حفظ القيمة بعد إدخالها وستُنقَل إلى صفحة تفاصيل نوع الكتاب: الخطأ الوحيد الذي نتحقق منه من طرف الخادم هو أن حقل نوع الكتاب يجب ألّا يكون فارغًا. توضح لقطة الشاشة التالية كيف ستبدو قائمة الأخطاء إن لم تقدّم نوع كتاب (المحدَّد باللون الأحمر): ملاحظة: تستخدم عملية التحقق من صحة البيانات التابع trim() لضمان عدم قبول المسافة البيضاء بوصفها اسمًا لنوع الكتاب، ويمكننا أيضًا التحقق من أن الحقل ليس فارغًا من طرف العميل من خلال إضافة القيمة required='true' إلى تعريف الحقل في الاستمارة. input#name.form-control(type='text', placeholder='Fantasy, Poetry etc.' name='name' value=(undefined===genre ? '' : genre.name), required='true' ) ترجمة -وبتصرُّف- للمقال Create genre form. اقرأ المزيد المقال السابق إنشاء موقع مكتبة محلية باستخدام Express. الاستمارات (forms) في متصفح الويب وكيفية التعامل معها في جافاسكربت. تطبيق عملي لتعلم جانغو - الجزء الثامن: العمل مع الاستمارات Forms. إرسال البيانات واستلامها عبر الشبكة في جافاسكريبت
-
سنتعرف في هذا المقال على كيفية إنشاء الصفحة الرئيسية لموقع المكتبة المحلية LocalLibrary وصفحات قوائم الكتب والمؤلفين ونسخ الكتب وأنواعها، وسنوضح كيفية تنسيق التاريخ باستخدام المكتبة Luxon، كما سنتعرف على كيفية إنشاء صفحات تفاصيل الكتب وأنواعها ونسخها وتفاصيل المؤلفين. صفحة الموقع الرئيسية ستكون الصفحة الأولى التي سننشئها هي الصفحة الرئيسية لموقع الويب، والتي يمكن الوصول إليها إما من جذر الموقع / أو من الدليل catalog/، مما سيؤدي إلى عرض بعض النصوص الثابتة التي تصف الموقع، مع الأعداد المحسوبة ديناميكيًا لأنواع السجلات المختلفة في قاعدة البيانات. أنشأنا مسبقًا وجهةً Route للصفحة الرئيسية، ولإكمال هذه الصفحة، يجب تحديث دالة المتحكم لجلب أعداد السجلات من قاعدة البيانات وإنشاء عرض (قالب) يمكننا استخدامه لعرض الصفحة. ملاحظة: سنستخدم مكتبة Mongoose للحصول على معلومات قاعدة البيانات، لذا لا بد من إعادة قراءة قسم البحث عن السجلات من مقال استخدام قاعدة البيانات باستخدام مكتبة Mongoose. الوجهة Route أنشأنا وِجهات صفحة الفهرس في مقال الوجهات والمتحكمات، إذ عرّفنا جميع دوال الوجهات في الملف "/routes/catalog.js": // الحصول على صفحة الدليل الرئيسية router.get("/", book_controller.index); //يُربَط مع /catalog/ لأننا نستورد الوجهة مع البادئة /catalog تملك دالة متحكم الكتاب index المُمرَّرة بوصفها معاملًا (book_controller.index) تقديم بديل placeholder implementation مُعرَّف في الملف /controllers/bookController.js: exports.index = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Site Home Page"); }); تُعَد هذه الدالة هي دالة المتحكم التي نوسّعها للحصول على معلومات من نماذجنا ثم عرضها باستخدام عرض (قالب). المتحكم Controller تحتاج دالة المتحكم index إلى جلب معلومات حول عدد سجلات الكتب Book ونسخ الكتب BookInstance (جميعها) ونسخ الكتب BookInstance (المتاحة) والمؤلفين Author وأنواع الكتب Genre الموجودة في قاعدة البيانات، وعرض هذه البيانات في قالب لإنشاء صفحة HTML، ثم إعادتها في استجابة HTTP. افتح الملف /controllers/bookController.js، إذ سترى الدالة index() المستورَدة بالقرب من أعلى الملف. const Book = require("../models/book"); const asyncHandler = require("express-async-handler"); exports.index = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Site Home Page"); }); ضع جزء الشيفرة التالي مكان الشيفرة السابقة، فأول شيء تفعله هذه الشيفرة هو استيراد (أو طلب require()) جميع النماذج Models، إذ نحتاج إلى ذلك لأننا سنستخدمها للحصول على عدد المستندات. تطلب الشيفرة أيضًا "express-async-handler" الذي يوفّر مغلِّفًا Wrapper لالتقاط الاستثناءات التي تُرمَى في دوال معالج الوِجهة. const Book = require("../models/book"); const Author = require("../models/author"); const Genre = require("../models/genre"); const BookInstance = require("../models/bookinstance"); const asyncHandler = require("express-async-handler"); exports.index = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل عدد الكتب ونسخها والمؤلفين وأنواع الكتب (على التوازي) const [ numBooks, numBookInstances, numAvailableBookInstances, numAuthors, numGenres, ] = await Promise.all([ Book.countDocuments({}).exec(), BookInstance.countDocuments({}).exec(), BookInstance.countDocuments({ status: "Available" }).exec(), Author.countDocuments({}).exec(), Genre.countDocuments({}).exec(), ]); res.render("index", { title: "Local Library Home", book_count: numBooks, book_instance_count: numBookInstances, book_instance_available_count: numAvailableBookInstances, author_count: numAuthors, genre_count: numGenres, }); }); نستخدم التابع countDocuments() للحصول على عدد نسخ كل نموذج، ويُستدعَى هذا التابع في نموذج مع مجموعة اختيارية من الشروط لمطابقتها وإعادة كائن استعلام Query. يمكن تنفيذ الاستعلام من خلال استدعاء التابع exec()، والذي يعيد وعدًا Promise إما يجري الوفاء به مع نتيجة أو يُرفض إذا كان هناك خطأ في قاعدة البيانات. تُعَد الاستعلامات الخاصة بأعداد المستندات مستقلة عن بعضها بعضًا، لذا نستخدم التابع Promise.all() لتنفيذها على التوازي، ويعيد هذا التابع وعدًا جديدًا ننتظره باستخدام await حتى ينتهي، إذ يتوقف التنفيذ مؤقتًا في هذه الدالة عند await. يجري الوفاء بالوعد الذي يعيده all() عند اكتمال جميع الاستعلامات، ويستمر تنفيذ دالة معالج الوجهة، ويملأ هذا الوعد المصفوفةَ بنتائج استعلامات قاعدة البيانات. نستدعي بعد ذلك التابع res.render()، ونحدّد عرضًا (قالبًا) بالاسم "index" وكائنات تربط نتائج استعلامات قاعدة البيانات مع قالب العرض، إذ تتوفر البيانات على شكل أزواج مفتاح-قيمة key-value، ويمكن الوصول إليها في القالب باستخدام المفتاح. لاحظ أن الشيفرة البرمجية بسيطة جدًا لأنه يمكننا افتراض نجاح استعلامات قاعدة البيانات، وإذا فشلت أيٌّ من عمليات قاعدة البيانات، فسيكتشف asyncHandler() الاستثناء المُرمَى ويمرّره إلى معالج البرمجية الوسيطة التالية next في السلسلة. العرض View افتح العرض "/views/index.pug" وضع فيه المحتوى التالي بدلًا من محتواه الموجود مسبقًا: extends layout block content h1= title p Welcome to #[em LocalLibrary], a very basic Express website developed as a tutorial example on the Mozilla Developer Network. h1 Dynamic content p The library has the following record counts: ul li #[strong Books:] !{book_count} li #[strong Copies:] !{book_instance_count} li #[strong Copies available:] !{book_instance_available_count} li #[strong Authors:] !{author_count} li #[strong Genres:] !{genre_count} يُعَد العرض واضحًا، إذ نوسّع القالب الأساسي layout.pug من خلال تعديل الكتلة block التي اسمها 'content'. سيكون العنوان h1 الأول هو النص المُهرَّب للمتغير title المُمرَّر إلى التابع render()، ولاحظ استخدام 'h1=' بحيث يجري التعامل مع النص الذي يليه بوصفه تعبير جافا سكريبت، ثم نضمّن فقرةً تعرّف بالمكتبة المحلية LocalLibrary. نسرد عدد النسخ من كل نموذج تحت عنوان المحتوى الديناميكي Dynamic content. لاحظ أن قيم القالب للبيانات هي المفاتيح المُحدَّدة عند استدعاء التابع render() في دالة معالج الوِجهة. ملاحظة: لم نهرّب قيم العد، إذ استخدمنا صيغة {}!، لأن قيم العد محسوبة، ولكن إذا وفّر المستخدمون النهائيون المعلومات، فسنهرّب المتغير لعرضه. كيف تبدو الصفحة الرئيسية؟ أنشأنا حتى الآن كل ما هو مطلوب لعرض صفحة الفهرس، لذا شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي: ملاحظة: لن تتمكّن من استخدام روابط الشريط الجانبي حاليًا لأن عناوين URL والعروض والقوالب الخاصة بهذه الصفحات غير مُعرَّفة بعد، وإذا حاولت الوصول إليها، فستحصل على أخطاء مثل "NOT IMPLEMENTED: Book list" اعتمادًا على الرابط الذي تنقر عليه، إذ تُحدَّد هذه السلسلة النصية الحرفية -التي سنضع مكانها البيانات المناسبة- في المتحكمات المختلفة التي تكون موجودة ضمن الملف "controllers". صفحة قائمة الكتب سنقدّم الآن صفحة قائمة الكتب، إذ تعرض هذه الصفحة قائمةً بجميع الكتب الموجودة في قاعدة البيانات مع مؤلفها والعنوان الخاص بكل كتاب، والذي يكون رابطًا تشعبيًا hyperlink لصفحة تفاصيل الكتاب المرتبطة به. المتحكم يجب أن تحصل دالة متحكم قائمة الكتب على قائمة بجميع كائنات الكتاب Book في قاعدة البيانات، ثم تفرزها وتمرّرها إلى القالب لعرضها. افتح الملف "/controllers/bookController.js"، وابحث عن تابع المتحكم book_list() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض قائمة جميع الكتب exports.book_list = asyncHandler(async (req, res, next) => { const allBooks = await Book.find({}, "title author") .sort({ title: 1 }) .populate("author") .exec(); res.render("book_list", { title: "Book List", book_list: allBooks }); }); يستدعي معالج الوجهة Route Handler الدالة find() للنموذج Book، ويختار إعادة العنوان title والمؤلف author فقط لأننا لسنا بحاجة إلى الحقول الأخرى (سيعيد أيضًا الحقل _id والحقول الافتراضية)، ويفرز النتائج حسب العنوان أبجديًا باستخدام التابع sort(). نستدعي أيضًا التابع populate() للنموذج Book مع تحديد الحقل author، مما سيؤدي إلى استبدال معرّف مؤلف الكتاب المُخزَّن بتفاصيل المؤلف الكاملة، ثم يُستدعَى التابع exec() في نهاية السلسلة التعاقبية daisy-chained لتنفيذ الاستعلام وإعادة الوعد. يستخدم معالج الوِجهة await لانتظار الوعد، مما يؤدي إلى إيقاف التنفيذ حتى استقراره. إذا جرى الوفاء بالوعد، فستُحفَظ نتائج الاستعلام في المتغير allBooks ويستمر المعالج في التنفيذ. يستدعي الجزء الأخير من معالج الوِجهة التابع render()، ويحدد القالب book_list (.pug) ويمرر قيم title و book_list إلى القالب. العرض View أنشئ العرض "/views/book_list.pug" وضع فيه المحتوى التالي: extends layout block content h1= title ul each book in book_list li a(href=book.url) #{book.title} | (#{book.author.name}) else li There are no books. يوسّع العرض القالبَ الأساسي layout.pug ويعدّل الكتلة block التي اسمها 'content'، ويعرض العنوان title الذي مرّرناه من المتحكم (باستخدام التابع render()) ويتكرر عبر المتغير book_list باستخدام صيغة each-in-else. يُنشَأ عنصر قائمة لكل كتاب، إذ تعرض هذه القائمة عنوان الكتاب كرابط إلى صفحة تفاصيل الكتاب متبوعًا باسم المؤلف، وإذا لم يكن هناك كتب في book_list، فستُنفَّذ تعليمة else، ويعرض نص عدم وجود كتب "There are no books". ملاحظة: نستخدم book.url لتوفير رابط إلى سجل تفاصيل كل كتاب (قدّمنا هذه الوجهة، ولكن لم نقدّم الصفحة الخاصة بها بعد)، وهي خاصية افتراضية لنموذج الكتاب Book الذي يستخدم الحقل _id الخاص بنسخة النموذج لإنشاء مسار عنوان URL فريد. من المهم تعريف كل كتاب على سطرين، باستخدام الشريط العمودي | في السطر الثاني، ويُعَد هذا الأسلوب ضروريًا لأنه إذا كان اسم المؤلف موجودًا في السطر السابق، فسيكون جزءًا من الرابط التشعبي. كيف تبدو صفحة قائمة الكتب؟ شغّل التطبيق (راجع قسم اختبار الوجهات من مقال الوجهات والمتحكمات للاطلاع على الأوامر ذات الصلة) وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدّد رابط "جميع الكتب All books". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي: صفحة قائمة نسخ الكتب BookInstance سنقدّم الآن قائمة بجميع نسخ الكتب BookInstance في المكتبة، إذ يجب أن تتضمن الصفحة عنوان الكتاب Book المرتبط بكل نسخة كتاب BookInstance (مرتبطة برابط إلى صفحة تفاصيلها) مع المعلومات الأخرى في نموذج BookInstance، بما في ذلك الحالة والناشر والمعرِّف الفريد لكل نسخة، ويجب ربط نص المعرّف الفريد برابط إلى صفحة تفاصيل نسخة الكتاب BookInstance. المتحكم يجب أن يحصل متحكم قائمة BookInstance على قائمة بجميع نسخ الكتاب، ويجب ملء معلومات الكتاب المرتبطة بها، ثم تمرير القائمة إلى القالب لعرضها. افتح الملف "/controllers/bookinstanceController.js"، وابحث عن تابع المتحكم bookinstance_list() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض قائمة جميع نسخ الكتب exports.bookinstance_list = asyncHandler(async (req, res, next) => { const allBookInstances = await BookInstance.find().populate("book").exec(); res.render("bookinstance_list", { title: "Book Instance List", bookinstance_list: allBookInstances, }); }); يستدعي معالج الوجهة الدالةَ find() للنموذج BookInstance، ثم يتبعه في سلسلة تعاقبية استدعاءٌ للتابع populate() مع حقل الكتاب book، والذي سيضع مستند كتاب Book كامل مكان معرّف الكتاب المُخزَّن لكل BookInstance، ثم يُستدعَى التابع exec() في نهاية السلسلة التعاقبية لتنفيذ الاستعلام وإعادة الوعد. يستخدم معالج الوِجهة await لانتظار الوعد، مما يؤدي إلى إيقاف التنفيذ حتى استقراره. إذا جرى الوفاء بالوعد، فستُحفَظ نتائج الاستعلام في المتغير allBookInstances ويستمر المعالج في التنفيذ. يستدعي الجزء الأخير من الشيفرة البرمجية التابع render()، ويحدد القالب bookinstance_list (.pug) ويمرر قيم title و bookinstance_list إلى القالب. العرض View أنشئ العرض "/views/bookinstance_list.pug" وضع فيه المحتوى التالي: extends layout block content h1= title ul each val in bookinstance_list li a(href=val.url) #{val.book.title} : #{val.imprint} - if val.status=='Available' span.text-success #{val.status} else if val.status=='Maintenance' span.text-danger #{val.status} else span.text-warning #{val.status} if val.status!='Available' span (Due: #{val.due_back} ) else li There are no book copies in this library. يماثل هذا العرض العروض الأخرى، إذ يوسّع التخطيط، ويبدّل كتلة المحتوى content، ويعرض العنوان title المُمرَّر من المتحكم، ويتكرر عبر جميع نسخ الكتاب في bookinstance_list. نعرض لكل نسخة حالتها، وإن لم يكن الكتاب متاحًا، فسيُعرَض تاريخ استرجاعه المتوقع. قدّمنا ميزة جديدة، إذ يمكننا استخدام الصيغة النقطية بعد الوسم لإسناد صنف، لذلك سيُصرَّف span.text-success إلى <span class="text-success">، ويمكن كتابته أيضًا في Pug بالشكل التالي: span(class="text-success") كيف تبدو صفحة قائمة نسخ الكتب؟ شغّل التطبيق، وافتح متصفحك على العنوان "http://localhost:3000/" ثم حدد رابط جميع نسخ الكتاب All book-instances. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي: تنسيق التاريخ باستخدام مكتبة Luxon لا يبدو العرض الافتراضي للتواريخ في نماذجنا جميلًا: Mon Apr 10 2020 15:49:58 GMT+1100 (AUS Eastern Daylight Time) لذا سنوضح في هذا القسم كيفية تحديث صفحة قائمة نسخ الكتاب BookInstance List من القسم السابق من أجل عرض حقل due_date بتنسيق أكثر ملاءمة مثل: Apr 10th, 2023. الطريقة التي سنستخدمها هي إنشاء خاصية افتراضية في نموذج BookInstance، والتي تعيد التاريخ المُنسَّق. سنطبّق التنسيق الفعلي باستخدام مكتبة Luxon، وهي مكتبة قوية وحديثة وسهلة الاستخدام لتحليل التواريخ والتحقق من صحتها ومعالجتها وتنسيقها وجعلها تواريخًا محلية. ملاحظة: يمكن استخدام مكتبة Luxon لتنسيق السلاسل النصية مباشرةً في قوالب Pug، أو يمكننا تنسيق السلسلة النصية في عدد من الأماكن الأخرى. يتيح استخدام خاصية افتراضية الحصول على التاريخ المُنسَّق باستخدام الطريقة نفسها للحصول على تاريخ الاسترجاع due_date حاليًا. تثبيت مكتبة Luxon أدخل الأمر التالي في جذر المشروع: npm install luxon إنشاء الخاصية الافتراضية افتح الملف "./models/bookinstance.js"، ثم استورد مكتبة Luxon في أعلى هذا الملف كما يلي: const { DateTime } = require("luxon"); ضِف الخاصية الافتراضية due_back_formatted بعد خاصية URL مباشرةً. BookInstanceSchema.virtual("due_back_formatted").get(function () { return DateTime.fromJSDate(this.due_back).toLocaleString(DateTime.DATE_MED); }); ملاحظة: يمكن لمكتبة Luxon استيراد السلاسل النصية بتنسيقات متعددة وتصديرها إلى كلٍّ من التنسيقات المُعرَّفة مسبقًا والتنسيقات الحرة free-form formats، إذ نستخدم في حالتنا التابع fromJSDate() لاستيراد سلسلة تاريخ جافا سكريبت و toLocaleString() لإخراج التاريخ بتنسيق DATE_MED باللغة الإنجليزية: Apr 10th, 2023. اطلع على توثيق مكتبة Luxon الخاص بالتنسيق للحصول على معلومات حول التنسيقات الأخرى وجعل سلسلة التاريخ عالمية. تحديث العرض افتح العرض "/views/bookinstance_list.pug" وضع due_back_formatted بدلًا من due_back. if val.status != 'Available' //span (Due: #{val.due_back} ) span (Due: #{val.due_back_formatted} ) إذا انتقلت إلى جميع نسخ الكتاب All book-instances في الشريط الجانبي، فيجب أن ترى أن جميع تواريخ الاسترجاع أجمل بكثير. صفحة قائمة المؤلفين يجب أن تعرض صفحة قائمة المؤلفين قائمة بجميع المؤلفين في قاعدة البيانات، مع ربط اسم كل مؤلف برابط إلى صفحة تفاصيل المؤلف المرتبطة به، ويجب إدراج تاريخ الميلاد وتاريخ الوفاة بعد الاسم الموجود على السطر نفسه. المتحكم يجب أن تحصل دالة متحكم قائمة المؤلفين على قائمة بجميع نسخ المؤلف Author، ثم تمرّرها إلى القالب لعرضها. افتح الملف "/controllers/authorController.js"، ثم ابحث عن تابع المتحكم المُصدَّر author_list() بالقرب من أعلى الملف وضع مكانه الشيفرة البرمجية التالية: // عرض قائمة جميع المؤلفين exports.author_list = asyncHandler(async (req, res, next) => { const allAuthors = await Author.find().sort({ family_name: 1 }).exec(); res.render("author_list", { title: "Author List", author_list: allAuthors, }); }); تتبع دالة متحكم الوجهة النمط نفسه المُتبَع في صفحات القوائم الأخرى، إذ تعرّف استعلامًا لنموذج المؤلف Author باستخدام دالة find() للحصول على جميع المؤلفين والتابع sort() لفرزهم حسب family_name بترتيب أبجدي، ثم يُستدعَى التابع exec() في نهاية السلسلة التعاقبية لتنفيذ الاستعلام وإعادة الوعد الذي يمكن أن تنتظره الدالة باستخدام await. يعرض معالج الوجهة قالب author_list(.pug) بعد الوفاء بالوعد، ويمرر عنوان title الصفحة وقائمة المؤلفين (allAuthors) باستخدام مفاتيح القالب. العرض أنشئ العرض "/views/author_list.pug" وضع المحتوى التالي بدل المحتوى الموجود مسبقًا: extends layout block content h1= title ul each author in author_list li a(href=author.url) #{author.name} | (#{author.date_of_birth} - #{author.date_of_death}) else li There are no authors. شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط جميع المؤلفين "All authors". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو الصفحة كما يلي: ملاحظة: لا يُعَد مظهر تواريخ العمر الافتراضي للمؤلف جميلًا، ويمكنك تحسينه باستخدام الأسلوب نفسه الذي استخدمناه في قائمة نسخ الكتب BookInstance من خلال إضافة الخاصية الافتراضية للعمر الخاص بنموذج المؤلف Author. يمكن ألا يكون المؤلف ميتًا أو يمكن أن يكون لديه بيانات ولادة أو وفاة مفقودة، لذا يجب تجاهل التواريخ المفقودة أو المراجع التي تشير إلى خاصيات غير موجودة. تتمثل إحدى طرق التعامل مع ذلك في إعادة إما تاريخ مُنسَّق أو سلسلة فارغة اعتمادًا على ما إذا كانت الخاصية مُعرَّفةً أم لا، مثل: return this.date_of_birth ? DateTime.fromJSDate(this.date_of_birth).toLocaleString(DateTime.DATE_MED) : ''; صفحة قائمة أنواع الكتب Genre - التحدي يجب عليك في هذا القسم تقديم صفحة قائمة أنواع الكتب، إذ يجب أن تعرض الصفحة قائمة بجميع أنواع الكتب الموجودة في قاعدة البيانات، مع ربط كل نوع برابط إلى صفحة التفاصيل المرتبطة به بحيث تحصل على الصفحة التالية: يجب أن تحصل دالة متحكم قائمة الأنواع على قائمة بجميع نسخ الأنواع Genre، ثم تمرّرها إلى القالب لعرضها باتباع الخطوات التالية: يجب تعديل التابع genre_list() في الملف "/controllers/genreController.js". يماثل تقديم هذا التابع تقريبًا متحكم التابع author_list()، ثم يجب فرز النتائج حسب الاسم بترتيب تصاعدي. يجب تسمية القالب المراد عرضه بالاسم genre_list.pug. يجب أن يمرر القالب المراد عرضه متغيرات العنوان title (الذي هو 'Genre List') و genre_list (قائمة أنواع الكتب المُعادة من دالة رد النداء Genre.find()). يجب أن يتطابق العرض مع لقطة الشاشة أو المتطلبات السابقة، إذ يجب أن يكون له بنية أو تنسيق مشابه جدًا لعرض قائمة المؤلفين باستثناء أن أنواع الكتب لا تحتوي على تواريخ. صفحة تفاصيل أنواع الكتب Genre يجب أن تعرض صفحة تفاصيل أنواع الكتب المعلومات الخاصة بنسخة نوع كتاب معينة باستخدام قيمة الحقل _id المُولَّد تلقائيًا بوصفه المعرِّف. يُرمَّز معرّف سجل نوع الكتاب المطلوب في نهاية عنوان URL ويُستخرَج تلقائيًا بناءً على تعريف الوجهة (/genre/:id)، ثم يجري الوصول إليه ضمن المتحكم باستخدام معاملات الطلب: req.params.id. يجب أن تعرض الصفحة اسم نوع الكتاب وقائمة بجميع الكتب في هذا النوع مع روابط إلى صفحة تفاصيل كل كتاب. المتحكم افتح الملف "/controllers/genreController.js" واطلب وحدة الكتاب Book في بداية الملف، إذ يجب أن يطلب الملف باستخدام الدالة require() الوحدة Genre و "express-async-handler". const Book = require("../models/book"); ابحث عن تابع المتحكم genre_detail() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض صفحة التفاصيل لنوع كتاب معين exports.genre_detail = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل نوع الكتاب وجميع الكتب المرتبطة به (على التوازي) const [genre, booksInGenre] = await Promise.all([ Genre.findById(req.params.id).exec(), Book.find({ genre: req.params.id }, "title summary").exec(), ]); if (genre === null) { // لا توجد نتائج const err = new Error("Genre not found"); err.status = 404; return next(err); } res.render("genre_detail", { title: "Genre Detail", genre: genre, genre_books: booksInGenre, }); }); نستخدم التابع Genre.findById() أولًا للحصول على معلومات نوع كتاب لمعرّف معين، والتابع Book.find() للحصول على جميع سجلات الكتب التي لها نفس معرّف نوع الكتاب المرتبط بها. لا يعتمد هذان الطلبان على بعضهما، لذا نستخدم التابع Promise.all() لتشغيل استعلامات قاعدة البيانات على التوازي، إذ وضّحنا هذا الأسلوب لتشغيل الاستعلامات على التوازي عندما أنشأنا [الصفحة الرئيسية](رابط المقال السابق)). ننتظر باستخدام await الوعد المُعاد، ثم نتحقق من النتائج عندما يستقر؛ فإذا لم يكن نوع الكتاب موجودًا في قاعدة البيانات (أي يمكن أن يكون محذوفًا)، فسيعيد التابع findById() بنجاح بدون نتائج، ونريد في هذه الحالة عرض صفحة عدم وجوده "not found"، لذلك ننشئ كائن خطأ Error ونمرّره إلى دالة البرمجية الوسيطة التالية next في السلسلة. ملاحظة: تنتقل الأخطاء -المُمرَّرة إلى دالة البرمجية الوسيطة التالية next- إلى شيفرة معالجة الأخطاء التي جرى إعدادها عندما أنشأنا التطبيق الهيكلي (اطلع على قسم معالجة الأخطاء لمزيد من المعلومات). إذا عُثِر على نوع الكتاب genre، فسنستدعي التابع render() لتقديم العرض، إذ يكون قالب العرض هو genre_detail (.pug). تُمرَّر قيم title و genre و booksInGenre إلى القالب باستخدام المفاتيح المقابلة (title و genre و genre_books). العرض أنشئ العرض "/views/genre_detail.pug" وضع فيه المحتوى التالي: extends layout block content h1 Genre: #{genre.name} div(style='margin-left:20px;margin-top:20px') h4 Books dl each book in genre_books dt a(href=book.url) #{book.title} dd #{book.summary} else p This genre has no books يشابه هذا العرض جميع قوالبنا الأخرى، ولكن يتمثل الاختلاف الرئيسي في أننا لا نستخدم title المُمرَّر إلى العنوان الأول، بالرغم من استخدامه في قالب layout.pug الأساسي لضبط عنوان الصفحة. كيف تبدو صفحة تفاصيل نوع الكتاب؟ شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط جميع أنواع الكتب "All genres"، ثم حدّد أحد هذه الأنواع مثل النوع "Fantasy". إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو صفحتك كما يلي: ملاحظة: يمكن أن تحصل على خطأ مشابه لما يلي: Cast to ObjectId failed for value " 59347139895ea23f9430ecbb" at path "_id" for model "Genre" وهو خطأ Mongoose سببه عدم تحويل req.params.id أو أي معرّف ID آخر إلى ()mongoose.Types.ObjectId، ويعود السبب الأكثر شيوعًا لذلك إلى اختلاف الرقم المعرّف عن المعرّف الحقيقي، ويمكن حل هذه المشكلة من خلال استخدام ()Mongoose.prototype.isValidObjectId لفحص المعرّف إذا كان صالحًا. صفحة تفاصيل الكتاب يجب أن تحصل صفحة تفاصيل الكتاب على المعلومات الخاصة بكتابٍ Book معين يُحدَّد باستخدام قيمة الحقل _id المُولَّد تلقائيًا، إضافةً إلى معلومات حول كل نسخة مرتبطة به في المكتبة BookInstance. ينبغي ربط أيّ مؤلف أو نوع أو نسخة كتاب نعرضها برابط إلى صفحة التفاصيل المرتبطة بهذا العنصر. المتحكم افتح الملف "/controllers/bookController.js"، وابحث عن تابع المتحكم book_detail() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض صفحة تفاصيل كتاب معين exports.book_detail = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل الكتب ونسخ الكتب لكتاب معين const [book, bookInstances] = await Promise.all([ Book.findById(req.params.id).populate("author").populate("genre").exec(), BookInstance.find({ book: req.params.id }).exec(), ]); if (book === null) { // لا توجد نتائج const err = new Error("Book not found"); err.status = 404; return next(err); } res.render("book_detail", { title: book.title, book: book, book_instances: bookInstances, }); }); ملاحظة: لا نحتاج إلى طلب أيّ وحدات إضافية حاليًا، إذ استوردنا مسبقًا الاعتماديات عندما قدّمنا متحكم الصفحة الرئيسية. استخدمنا الأسلوب نفسه الموضح في صفحة تفاصيل نوع الكتاب، إذ تستخدم دالةُ متحكم الوجهة التابعَ Promise.all() للاستعلام عن الكتاب Book المُحدَّد والنسخ المرتبطة به BookInstance على التوازي. إذا لم يُعثَر على كتاب مطابق، فسيُعاد كائن خطأ مع الخطأ "404: Not Found"، وإذا عُثِر على الكتاب، فستُعرَض معلومات قاعدة البيانات المسترجَعة باستخدام قالب "book_detail". نمرر results.book.title أثناء عرض صفحة الويب، نظرًا لاستخدام المفتاح "title" لإعطاء اسم لصفحة الويب كما هو مُعرَّف في ترويسة القالب "layout.pug". العرض أنشئ العرض "/views/book_detail.pug" وضِف إليه المحتوى التالي: extends layout block content h1 Title: #{book.title} p #[strong Author:] a(href=book.author.url) #{book.author.name} p #[strong Summary:] #{book.summary} p #[strong ISBN:] #{book.isbn} p #[strong Genre:] each val, index in book.genre a(href=val.url) #{val.name} if index < book.genre.length - 1 |, div(style='margin-left:20px;margin-top:20px') h4 Copies each val in book_instances hr if val.status=='Available' p.text-success #{val.status} else if val.status=='Maintenance' p.text-danger #{val.status} else p.text-warning #{val.status} p #[strong Imprint:] #{val.imprint} if val.status!='Available' p #[strong Due back:] #{val.due_back} p #[strong Id:] a(href=val.url) #{val._id} else p There are no copies of this book in the library. وضّحنا كل شيء تقريبًا في هذا القالب سابقًا، فلا حاجة للإعادة. ملاحظة: يمكن تقديم قائمة أنواع الكتب المرتبطة بالكتاب في القالب على النحو التالي، إذ تُضاف فاصلة بعد كل نوع مرتبط بالكتاب باستثناء النوع الأخير: p #[strong Genre:] each val, index in book.genre a(href=val.url) #{val.name} if index < book.genre.length - 1 |, كيف تبدو صفحة تفاصيل الكتاب؟ شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط جميع الكتب All books، وحدد أحد هذه الكتب. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو صفحتك كما يلي: صفحة تفاصيل المؤلف يجب أن تعرض صفحة تفاصيل المؤلف معلومات المؤلف Author، والذي يُحدَّد باستخدام قيمة الحقل _id المُولَّد تلقائيًا، إضافةً إلى قائمة بجميع كائنات الكتاب Book المرتبطة بهذا المؤلف Author. المتحكم افتح الملف "/controllers/authorController.js"، ثم ضِف السطر التالي إلى بداية الملف لطلب -باستخدام الدالة require()- الوحدة Book التي تحتاجها صفحة تفاصيل المؤلف، ويجب أن تكون الوحدات الأخرى مثل "express-async-handler" موجودة مسبقًا. const Book = require("../models/book"); ابحث عن تابع المتحكم author_detail() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض صفحة تفاصيل مؤلف مُحدَّد exports.author_detail = asyncHandler(async (req, res, next) => { // الحصول على تفاصيل المؤلف وجميع كتبه (على التوازي) const [author, allBooksByAuthor] = await Promise.all([ Author.findById(req.params.id).exec(), Book.find({ author: req.params.id }, "title summary").exec(), ]); if (author === null) { // لا توجد نتائج const err = new Error("Author not found"); err.status = 404; return next(err); } res.render("author_detail", { title: "Author Detail", author: author, author_books: allBooksByAuthor, }); }); يماثل هذا الأسلوب المتبع تمامًا الأسلوب الموضح في صفحة تفاصيل نوع الكتاب، إذ تستخدم دالةُ متحكم الوجهة التابعَ Promise.all() للاستعلام عن المؤلف Author ونسخ الكتب Book المرتبطة بها على التوازي. إذا لم يُعثَر على مؤلف مطابق، فسيُرسَل كائن خطأ إلى برمجية Express الوسيطة لمعالجة الأخطاء، وإذا عُثِر على المؤلف، فستُعرَض معلومات قاعدة البيانات المُسترجَعة باستخدام قالب تفاصيل المؤلف "author_detail". العرض أنشئ العرض "/views/author_detail.pug" وضع فيه المحتوى التالي: extends layout block content h1 Author: #{author.name} p #{author.date_of_birth} - #{author.date_of_death} div(style='margin-left:20px;margin-top:20px') h4 Books dl each book in author_books dt a(href=book.url) #{book.title} dd #{book.summary} else p This author has no books. وضّحنا كل شيء تقريبًا في هذا القالب سابقًا، فلا حاجة للإعادة. كيف تبدو صفحة تفاصيل المؤلف؟ شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدد رابط جميع المؤلفين All Authors، ثم حدد أحد المؤلفين. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن يبدو موقعك كما يلي: ملاحظة: لا تبدو تواريخ عمر المؤلف جميلة، لذا سنحسّنها في التحدي الأخير من هذا المقال. صفحة تفاصيل نسخ الكتب BookInstance يجب أن تعرض صفحة تفاصيل نسخ الكتاب BookInstance المعلومات الخاصة بكل نسخة كتاب والمُحدَّدة باستخدام قيمة الحقل _id المُولَّد تلقائيًا، إذ تتضمن هذه المعلومات اسم الكتاب Book بوصفه رابطًا إلى صفحة تفاصيل الكتاب، إضافةً إلى معلومات أخرى في السجل. المتحكم افتح الملف "/controllers/bookinstanceController.js"، ثم ابحث عن تابع المتحكم bookinstance_detail() المُصدَّر وضع مكانه الشيفرة البرمجية التالية: // عرض صفحة تفاصيل نسخة كتاب معينة exports.bookinstance_detail = asyncHandler(async (req, res, next) => { const bookInstance = await BookInstance.findById(req.params.id) .populate("book") .exec(); if (bookInstance === null) { // لا توجد نتائج const err = new Error("Book copy not found"); err.status = 404; return next(err); } res.render("bookinstance_detail", { title: "Book:", bookinstance: bookInstance, }); }); يشابه هذا التقديم ما استخدمناه لصفحات تفاصيل النماذج الأخرى، إذ تستدعي دالة متحكم الوجهة التابع BookInstance.findById() مع معرّف نسخة كتاب معين مُستخرَج من عنوان URL (باستخدام الوجهة Route)، ويمكن الوصول إليه في المتحكم باستخدام معاملات الطلب: req.params.id، ثم تستدعي التابع populate() للحصول على تفاصيل الكتاب Book المرتبط به. إذا لم يُعثَر على نسخة كتاب BookInstance مطابقة، فسيُرسَل خطأ إلى برمجية Express الوسيطة، وإلّا فستُعرَض البيانات المُعادة باستخدام طريقة العرض bookinstance_detail.pug. العرض أنشئ العرض "/views/bookinstance_detail.pug" وضع فيه المحتوى التالي: extends layout block content h1 ID: #{bookinstance._id} p #[strong Title:] a(href=bookinstance.book.url) #{bookinstance.book.title} p #[strong Imprint:] #{bookinstance.imprint} p #[strong Status:] if bookinstance.status=='Available' span.text-success #{bookinstance.status} else if bookinstance.status=='Maintenance' span.text-danger #{bookinstance.status} else span.text-warning #{bookinstance.status} if bookinstance.status!='Available' p #[strong Due back:] #{bookinstance.due_back} وضّحنا كل شيء تقريبًا في هذا القالب سابقًا، فلا حاجة للإعادة. كيف تبدو صفحة تفاصيل نسخ الكتاب؟ شغّل التطبيق وافتح متصفحك على العنوان "http://localhost:3000/"، ثم حدّد رابط جميع نسخ الكتب All book-instances، وحدّد أحد هذه العناصر. إذا جرى إعداد كل شيء بصورة صحيحة، فيجب أن تبدو موقعك كما يلي: تحدى نفسك تستخدم معظم التواريخ المعروضة حاليًا على الموقع تنسيق جافا سكريبت الافتراضي مثل: Tue Oct 06 2020 15:49:58 GMT+1100 (AUS Eastern Daylight Time) لذا يتمثل التحدي في هذا المقال في تحسين مظهر عرض التاريخ لمعلومات عمر المؤلف Author (تاريخ الوفاة والميلاد) ولصفحات تفاصيل نسخ الكتب لاستخدام التنسيق Oct 6th, 2016. ملاحظة: يمكنك استخدام نفس الأسلوب الذي استخدمناه لقائمة نسخ الكتب في المقال السابق من خلال إضافة خاصية العمر Lifespan الافتراضية إلى نموذج المؤلف Author واستخدام مكتبة Luxon لتنسيق سلاسل التاريخ. يجب عليك اتباع الخطوات التالية لإكمال هذا التحدي: ضع المتغير due_back_formatted مكان due_back في صفحة تفاصيل نسخ الكتاب. حدّث نموذج المؤلف Author لإضافة خاصية العمر الافتراضية، إذ يجب أن يبدو مثل: date_of_birth - date_of_death، ويكون لكلا القيمتين تنسيق التاريخ BookInstance.due_back_formatted. استخدم Author.lifespan في جميع العروض حيث تستخدم حاليًا date_of_birth و date_of_death صراحةً. ترجمة -وبتصرُّف- للمقالات Home page و Book list page و BookInstance list page و Date formatting using luxon و Author list page and Genre list page challenge و Genre detail page و Book detail page و Author detail page و BookInstance detail page and challenge. اقرأ المزيد المقال السابق مقدمة إلى القوالب Template في Express: إنشاء القالب الأساسي لموقع مكتبة محلية مثالًا. تطبيق عملي لتعلم Express - الجزء الأول: إنشاء موقع ويب هيكلي لمكتبة محلية. تطبيق عملي لتعلم إطار عمل جانغو - الجزء الأول: إنشاء موقع ويب هيكلي لمكتبة محلية.
-
القالب Template هو ملف نصي يحدّد بنية أو تخطيط ملف الخرج مع استخدام العناصر البديلة لتمثيل مكان إدراج البيانات عند عرض القالب، ويُشار إلى القوالب في إطار عمل Express بوصفها عروضًا Views. خيارات قوالب Express يمكن استخدام إطار عمل Express مع العديد من محرّكات عرض القوالب المختلفة، إذ سنستخدم في هذا المقال Pug (المعروفة سابقًا باسم Jade) لقوالبنا، والتي تُعَد لغة قوالب Node الأكثر شيوعًا، وتصف نفسها بأنها صيغة نظيفة وحساسة للمسافات لكتابة شيفرة HTML وهي متأثرة كثيرًا بلغة Haml. تستخدم لغات القوالب المختلفة طرقًا مختلفة لتعريف التخطيط Layout وتحديد العناصر البديلة للبيانات، إذ يستخدم بعضها شيفرة HTML لتعريف التخطيط، ويستخدم بعضها الآخر تنسيقات توصيف مختلفة يمكن تحويلها إلى شيفرة HTML. تُعَد Pug من النوع الثاني، إذ تستخدم تمثيلًا للغة HTML، إذ تُمثَّل الكلمةُ الأولى في أيّ سطر عنصرَ HTML، وتُستخدَم المسافة البادئة في الأسطر اللاحقة لتمثيل التداخل، وتكون النتيجة تعريف صفحة تُترجَم مباشرةً إلى شيفرة HTML، ولكنها أكثر إيجازًا وأسهل قراءة. ملاحظة: سلبية استخدام Pug أنها حساسة للمسافات البادئة والمسافات البيضاء، فإذا أضفتَ مسافة إضافية في المكان الخطأ، فقد تحصل على رمز خطأ غير مساعد، ولكن يصبح من السهل جدًا قراءة قوالبك وصيانتها بعد الانتهاء منها. ضبط القوالب ضبطنا موقع المكتبة المحلية LocalLibrary لاستخدام Pug عندما أنشأنا موقع الويب الهيكلي، إذ يجب أن تُضمَّن وحدة pug بوصفها اعتمادية في ملف package.json الخاص بموقع الويب، ويجب أن ترى إعدادات الضبط التالية في الملف app.js؛ إذ تخبرنا هذه الإعدادات أننا نستخدم Pug بوصفها محرّك عرض، وأن إطار عمل Express يجب أن يبحث عن القوالب في المجلد الفرعي "/views". // إعداد محرك العروض app.set("views", path.join(__dirname, "views")); app.set("view engine", "pug"); إذا بحثتَ في مجلد العروض views، فسترى ملفات ".pug" للعروض الافتراضية الخاصة بالمشروع، ويتضمن ذلك عرض الصفحة الرئيسية index.pug والقالب الأساسي layout.pug الذي يجب أن نضع مكانه محتوانا. /express-locallibrary-tutorial //the project root /views error.pug index.pug layout.pug صيغة القوالب يوضّح ملف القالب التالي العديد من ميزات Pug الأكثر فائدة، فأول شيء يجب ملاحظته هو أن الملف يربط بنية ملف HTML نموذجي مع الكلمة الأولى في كل سطر (تقريبًا) التي تكون عنصر HTML، ومع المسافة البادئة التي تُستخدَم للإشارة إلى العناصر المتداخلة، فمثلًا يوجد عنصر body داخل عنصر html، وتوجد عناصر الفقرة (p) داخل عنصر body وغير ذلك، وتكون العناصر غير المتداخلة مثل الفقرات الفردية موجودةً في أسطر منفصلة. doctype html html(lang="en") head title= title script(type='text/javascript'). body h1= title p This is a line with #[em some emphasis] and #[strong strong text] markup. p This line has un-escaped data: !{'<em> is emphasized</em>'} and escaped data: #{'<em> is not emphasized</em>'}. | This line follows on. p= 'Evaluated and <em>escaped expression</em>:' + title <!-- مباشرةً HTML يمكنك إضافة تعليقات--> // يمكنك إضافة تعليقات جافاسكربت ذات سطر واحد وتُنشَأ في تعليقات HTML //- يضمن تقديم تعليق جافاسكربت مؤلف من سطر واحد مع "-//" عدم عرض التعليق بوصفه شيفرة HTML p A line with a link a(href='/catalog/authors') Some link text | and some extra text. #container.col if title p A variable named "title" exists. else p A variable named "title" does not exist. p. Pug is a terse and simple template language with a strong focus on performance and powerful features. h2 Generate a list ul each val in [1, 2, 3, 4, 5] li= val تُعرَّف سمات العنصر بين أقواس بعد العنصر المرتبط بها، وتُعرَّف السمات ضمن الأقواس في قوائم مفصولٌ بينها بفواصل أو بمسافات بيضاء لأزواج أسماء السمات وقيمها مثل: script(type='text/javascript'), link(rel='stylesheet', href='/stylesheets/style.css') أو meta(name='viewport' content='width=device-width initial-scale=1') جرى تهريب escaped قيم جميع السمات مثل تحويل محارف مثل المحرف "<" إلى ما يكافئها من شيفرة HTML مثل ">" لمنع هجمات حقن شيفرة جافا سكريبت أو هجمات السكريبتات العابرة للمواقع cross-site scripting attacks - أو اختصارًا XSS. إذا كان الوسم متبوعًا بعلامة مساواة، فسيُتعامَل مع النص التالي بوصفه تعبير جافا سكريبت، فمثلًا سيكون محتوى الوسم h1 في السطر الأول متغير title (إما مُعرَّفًا في الملف أو مُمرَّرًا إلى القالب من إطار عمل Express). يكون محتوى الفقرة في السطر الثاني سلسلة نصية متعاقبة مع المتغير title. يكون السلوك الافتراضي في كلتا الحالتين هو تهريب السطر. h1= title p= 'Evaluated and <em>escaped expression</em>:' + title إذا لم يكن هناك رمز مساواة بعد الوسم، فسيُتعامَل مع المحتوى بوصفه نصًا عاديًا، إذ يمكنك ضمن النص العادي إدخال البيانات التي جرى تهريبها والتي لم يجرِ تهريبها باستخدام الصيغتين {}# و {}! على التوالي، ويمكنك أيضًا إضافة شيفرة HTML خام ضمن النص العادي. p This is a line with #[em some emphasis] and #[strong strong text] markup. p This line has an un-escaped string: !{'<em> is emphasized</em>'}, an escaped string: #{'<em> is not emphasized</em>'}, and escaped variables: #{title}. ملاحظة: سترغب دائمًا في تهريب البيانات من المستخدمين باستخدام صيغة {}#، وقد تُعرَض البيانات التي يمكن الوثوق بها، مثل أعداد السجلات المُولَّدة وما إلى ذلك دون تهريب القيم. يمكنك استخدام محرف الشريط العمودي ('|') في بداية السطر للإشارة إلى النص العادي، فمثلًا سيُعرَض النص الإضافي التالي على سطر الرابط نفسه، ولكنه لن يمثل رابطًا: a(href='http://someurl/') Link text | Plain text تسمح Pug بإجراء عمليات شرطية باستخدام if و else و else if و unless مثل: if title p A variable named "title" exists else p A variable named "title" does not exist يمكنك أيضًا إجراء عمليات الحلقة أو التكرار باستخدام صيغة each-in أو while، إذ أنشأنا في جزء الشيفرة البرمجية التالي حلقةً تكرارية تمر عبر عناصر مصفوفة لعرض قائمة متغيرات. لاحظ استخدام ''li=' لتقييم "val" بوصفه متغيرًا، ويمكن أيضًا تمرير القيمة التي تكررها إلى القالب بوصفها متغيرًا. ul each val in [1, 2, 3, 4, 5] li= val تدعم الصيغة أيضًا التعليقات (التي يمكن عرضها في الخرج أو عدم عرضها وفق ما تريده)، والمخاليط Mixins لإنشاء كتل من الشيفرة البرمجية قابلة لإعادة الاستخدام، وتعليمات الحالة، والعديد من الميزات الأخرى. اطلع على توثيق Pug لمزيد من المعلومات. توسيع القوالب من المعتاد أن يكون لجميع الصفحات بنية مشتركة عبر الموقع، بما في ذلك شيفرة HTML المعيارية لرأس وتذييل الصفحة وقسم التنقل navigation وغير ذلك، لذا تسمح Pug بالتصريح عن قالب أساسي ثم توسيعه، وتبديل الأجزاء المختلفة من كل صفحة بدلًا من إجبار المطورين على تكرار هذه الشيفرة المتداولة "Boilerplate" في كل صفحة. يبدو مثلًا القالب الأساسي layout.pug الذي أنشأناه في المشروع الهيكلي كما يلي: doctype html html head title= title link(rel='stylesheet', href='/stylesheets/style.css') body block content يُستخدَم الوسم block لتمييز أقسام المحتوى التي يمكن استبدالها في قالب مشتق، فإن لم يُعاد تعريف الكتلة، فسيُستخدَم تقديمها في الصنف الأساسي. يوضح القالب index.pug الافتراضي -الذي أنشأناه لمشروعنا الهيكلي- كيفية تعديل القالب الأساسي، إذ يحدّد الوسم extends القالب الأساسي المراد استخدامه، ثم نستخدم block section_name للإشارة إلى المحتوى الجديد للقسم الذي سنعدّله. extends layout block content h1= title p Welcome to #{title} القالب الأساسي لموقع المكتبة المحلية LocalLibrary فهمنا كيفية توسيع القوالب باستخدام Pug، وسنبدأ الآن بإنشاء قالب أساسي لمشروعنا، والذي سيحتوي على شريط جانبي مع روابط للصفحات التي نريد إنشائها في المقالات لاحقًا مثل صفحات عرض وإنشاء الكتب وأنواعها والمؤلفين وغير ذلك، وسيحتوي على منطقة للمحتوى الرئيسي سنعدّلها في كل صفحة من صفحاتنا. افتح القالب /views/layout.pug وضع الشيفرة التالية مكان المحتوى الموجود مسبقًا: doctype html html(lang='en') head title= title meta(charset='utf-8') meta(name='viewport', content='width=device-width, initial-scale=1') link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css", integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N", crossorigin="anonymous") script(src="https://code.jquery.com/jquery-3.5.1.slim.min.js", integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj", crossorigin="anonymous") script(src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.min.js", integrity="sha384-+sLIOodYLS7CIrQpBjl+C7nPvqq+FbNUBDunl/OZv93DB7Ln/533i8e/mZXLi/P+", crossorigin="anonymous") link(rel='stylesheet', href='/stylesheets/style.css') body div(class='container-fluid') div(class='row') div(class='col-sm-2') block sidebar ul(class='sidebar-nav') li a(href='/catalog') Home li a(href='/catalog/books') All books li a(href='/catalog/authors') All authors li a(href='/catalog/genres') All genres li a(href='/catalog/bookinstances') All book-instances li hr li a(href='/catalog/author/create') Create new author li a(href='/catalog/genre/create') Create new genre li a(href='/catalog/book/create') Create new book li a(href='/catalog/bookinstance/create') Create new book instance (copy) div(class='col-sm-10') block content يتضمن القالب شيفرة جافا سكريبت و CSS من بوتستراب Bootstrap لتحسين تخطيط وعرض صفحة HTML؛ إذ يُعَد استخدام بوتستراب أو أي إطار عمل ويب آخر من طرف العميل طريقةً سريعة لإنشاء صفحة جذابة يمكن أن تتناسب مع أحجام المتصفحات المختلفة، ويتيح أيضًا التعامل مع عرض الصفحة دون الحاجة إلى الدخول في التفاصيل، إذ نريد فقط التركيز على الشيفرة البرمجية من طرف الخادم حاليًا. ملاحظة: تكون السكربتات مُحمَّلة على أصول مختلطة، لذا يجب السماح صراحةً بتحميل هذه الملفات لاحقًا عندما نضيف برمجية وسيطة للأمان. اطلع على فقرة استخدام حزمة Helmet للحماية من الثغرات المعروفة من مقال النشر في بيئة الإنتاج لمزيد من المعلومات. يجب أن يكون التخطيط واضحًا إلى حدٍ ما إذا قرأت القسم السابق. لاحظ استخدام كتلة block content بوصفها عنصرًا بديلًا للمكان الذي سيوضع فيه محتوى صفحاتنا. يشير القالب الأساسي أيضًا إلى ملف CSS محلي (style.css) الذي يوفر بعض التنسيق الإضافي. افتح الملف "/public/stylesheets/style.css" وضع شيفرة CSS التالية مكان محتواه: .sidebar-nav { margin-top: 20px; padding: 0; list-style: none; } لدينا الآن قالب أساسي لإنشاء صفحات مع شريط جانبي، وسنستخدمه في المقالات التالية لتعريف الصفحات الفردية. ترجمة -وبتصرُّف- للمقالين Template primer و LocalLibrary base template. اقرأ المزيد المقال السابق: تطبيق عملي لتعلم Express - الجزء الخامس: النشر في بيئة الإنتاج كيفية استخدام القوالب في تطبيقات فلاسك Flask. مدخل إلى إطار عمل الويب Express وبيئة Node مدخل إلى express إنشاء مدوّنة باستخدام Node.js و Express (الجزء الأول)
-
تعرّفنا في المقال السابق على جمع المصفوفات العمودية الذي يُعَد إجراءً سهلًا، وسنوضّح في هذا المقال سبب كون هذا الإجراء مفيدًا من خلال مناقشة كيفية تمثيله لعملية جمع الأشعة. سنتعرّف في هذا المقال على المواضيع التالية: جمع الأشعة. جمع الأشعة باستخدام قاعدة الرأس إلى الذيل Head to Tail. قاعدة الرأس إلى الذيل في فضاء ثلاثي الأبعاد. الخاصية التجميعية Associative لجمع الأشعة. الخاصية التبديلية Commutative لجمع الأشعة. الإطارات الإحداثية Coordinate Frames. تمثيل النقاط الهندسية باستخدام إطارات الإحداثيات والمصفوفات العمودية. تمثيل الأشعة باستخدام المصفوفات العمودية. جمع الأشعة باستخدام المصفوفات العمودية. استخدام متوازي الأضلاع في عملية جمع الأشعة. متراجحة المثلث Triangle Inequality. الأشعة المرتبطة الخطية Collinear. جمع الأشعة الصفرية. عملية جمع شعاع ونقطة. تذكير: اجمع المصفوفتين العموديتين التاليتين: a = ( 3, 2 )T b = (-2, 1 )T والجواب هو: a + b = ( 1, 3 )T الأشعة في الفضاء ثنائي الأبعاد تُعَد الأشعة كائنات هندسية يمكن تمثيلها بطرق متعددة، ولكن يجب أن نفهم الفرق بين الكائن الهندسي (مثل النقطة أو الشعاع)، وطريقة تمثيله (باستخدام مصفوفة عمودية غالبًا). يوضّح المخطط البياني السابق النقاط A و B و C في فضاء ثنائي الأبعاد؛ حيث يُعبَّر عن الإزاحة Displacement بمسافة واتجاه، فالشعاع u هو الإزاحة من النقطة A إلى النقطة B، أما الشعاع v فهو الإزاحة من النقطة B إلى النقطة C. بينما يعبٌر الشعاع w عن الإزاحة من النقطة A إلى النقطة C؛ ويكون تأثير التحرك عبر الإزاحة u ثم عبر الإزاحة v هو تأثير التحرك عبر الإزاحة w نفسه وبالتالي يمكن القول أن: u + v = w عدم اعتمادية عملية جمع الأشعة على موضعها هل تعتمد عملية جمع الأشعة على مكان وجودها؟ لا، فليس للأشعة موضع محدّد. توضّح النقاط في المخطط البياني السابق كيفية حساب التأثير المشترك لإزاحتين من خلال جمعهما، ولكن لم يتضمّن جمعُ الإزاحات النقاط فعليًا، حيث يظهر جمع الأشعة عادةً كما في المخطط البياني السابق، الذي يوضح أن تأثير التحرك عبر الإزاحة u ثم التحرك عبر الإزاحة v هو تأثير التحرك عبر الإزاحة w نفسه. لنفترض مثلًا أنك ترغب في نقل جميع نقاط كائن هندسي عبر الإزاحة u، ثم عبر الإزاحة v. يمكنك تطبيق ذلك من خلال تحريك كل نقطة عبر الإزاحة w. قاعدة الرأس إلى الذيل Head-to-Tail جرب رسم الخط الذي يمثل مجموع الشعاعين s و t: والحل باستخدام قاعدة الرأس إلى الذيل هو: يمكن جمع الشعاعين v و u في المخطط البياني التالي من خلال تحريك الشعاع v مع الحفاظ على طوله ومنحاه Orientation حتى يلمس ذيلُ هذا الشعاع رأسَ الشعاع u، والناتج هو الشعاع المتشكّل من ذيل الشعاع u إلى رأس الشعاع v: اجمع الشعاعين e و d: والحل هو: يوضّح المخطط البياني السابق تحريك الشعاع e للأعلى حتى يلامس ذيله رأس الشعاع d، والناتج هو الشعاع المتشكّل من ذيل الشعاع d إلى رأس الشعاع e . جمع الأشعة في الفضاء ثلاثي الأبعاد يوضّح المخطط البياني التالي جمع الشعاعين a و b ثلاثيّي الأبعاد لتشكيل الشعاع c. وضعنا الصندوق ثلاثي الأبعاد للمساعدة على تصوّر الأبعاد الثلاثية، إذ تعمل قاعدة الرأس إلى الذيل في الفضاء ثلاثي الأبعاد أيضًا. تحدّد قاعدة الرأس إلى الذيل مثلثًا مُوجَّهًا في الفضاء ثلاثي الأبعاد عند جمع شعاعين في فضاء ثلاثي الأبعاد. الخاصية التجميعية Associative لجمع الأشعة هل يمكن جمع ثلاثة أشعة مثل الأشعة a و b و d في المخطط البياني التالي؟ نعم يمكن ذلك، والنتيجة هي الإزاحة الإجمالية الناتجة عن اتباع كل شعاع بدوره. يوضّح المخطط البياني التالي نتيجة جمع (a + b) + d = c + d، والنتيجة هي شعاع بطول واتجاه مماثل لقطر الشكل. يوضّح المخطط البياني التالي نتيجة جمع a + (b + d)، والنتيجة هي نفسها، ويُعَد ذلك دليلًا على الخاصية التجميعية لجمع الأشعة: a + (b + c) = (a + b) + c يمكن تطبيق هذه القاعدة في جميع الأبعاد، إذ تكون الأشعة الهندسية للرسوميات الحاسوبية غالبًا ثنائية وثلاثية الأبعاد بالرغم من إمكانية وجود أبعاد أعلى بصورة عامة. تعني الخاصية التجميعية أنه يمكن كتابة مجموع عدة أشعة مثل: a + b + c + d + e بدون قوسين. الخاصية التبديلية Commutative لجمع الأشعة لنفترض أنك تمشي مسافة خمس بنايات شمالًا، ثم ثلاث بنايات شرقًا، ولكن إذا مشيت ثلاث بنايات شرقًا ثم خمس بنايات شمالًا، فهل ستصل إلى المكان نفسه؟ نعم، هذا صحيح. يُعَد جمع الأشعة عملية تبديلية مثل جمع الأعداد الحقيقية تمامًا: a + b = b + a إذا بدأت من النقطة P، فسينتهي بك الأمر في المكان نفسه بغض النظر عن الإزاحة (a أو b) التي ستأخذها أولًا. سينتج الشعاع c عن كلٍّ من عمليتي الجمع a + b و b + a عند تطبيق قاعدة الرأس إلى الذيل، ولكن هل يمكن تطبيق الخاصية التبديلية على جميع الأبعاد؟ نعم، يمكن ذلك. تمثيل النقاط باستخدام المصفوفات العمودية يمكن تمثيل النقاط باستخدام المصفوفات العمودية، ولكن يجب أولًا اتخاذ قرار بشأن الإطار الإحداثي Coordinate Frame الذي يُسمَّى أحيانًا بالإطار فقط. ويتكون الإطار الإحداثي من نقطة مميزة P0 تسمى نقطة الأصل Origin، ومن محور لكل بُعد يُسمَّى X و Y غالبًا، فهناك محوران في الفضاء ثنائي الأبعاد، وثلاثة محاور تُسمَّى X و Y و Z في الفضاء ثلاثي الأبعاد. لنبدأ بالفضاء ثنائي الأبعاد، حيث يُظهِر المخطط البياني الأيسر عالمًا افتراضيًا بسيطًا إلى حدٍ ما في فضاء ثنائي الأبعاد، إذ تكون النقاط والأشعة موجودةً في الفضاء بصورة مستقلة عن أيّ إطار إحداثي، بينما يوضّح المخطط البياني الأوسط العالم الافتراضي نفسه مع إطار إحداثي يتكون من نقطة معينة P0 ومحورين، وتُمثَّل النقطة A في هذا الإطار الإحداثي بالمصفوفة العمودية (2, 2)T. يُظهر المخطط البياني الأيمن العالم الافتراضي نفسه مع إطار إحداثي مختلف، إذ تُمثَّل النقطة A في هذا الإطار الإحداثي بالمصفوفة العمودية (2, 3)T. إذًا، كيف تُمثَّل النقطة B باستخدام الإطار الإحداثي الأول، وباستخدام الإطار الإحداثي الثاني؟ تُمثَّل النقطة B باستخدام الإطار الإحداثي الأول بالمصفوفة العمودية (6, 2)T، وتُمثَّل باستخدام الإطار الإحداثي الثاني بالمصفوفة العمودية (4.8, 1.6)T. تمثيل النقاط باستخدام إطارات إحداثية مختلفة الفكرة الأساسية -والمربكة في كثير من الأحيان- هي أنه يوجد في برنامج الرسوميات عالم افتراضي واحد، ولكن قد يكون هناك أيّ عددٍ من الإطارات الإحداثية قيد الاستخدام، إذ لكل كائنٍ إطاره الخاص غالبًا، بحيث يُستخدَم إطار مختلف لوصف مواقع الكائنات؛ ويمكن استخدام إطارٍ آخر لمجال الرؤية، ويُعَد ذلك شيئًا تفعله فعليًا طوال الوقت مع العالم الحقيقي، ولكنه يكون الجزء الصعب في معرفة كيفية العمل مع العالم الافتراضي. لنفترض أن لديك مزهريةً موضوعةً على طاولة مثلًا، وأنت تتحدث مع صديق عبر الهاتف وترغب في وصف المزهرية له ومكانها وكيف تبدو لك. يمكنك وصف المزهرية بأنها كرة مركزها نقطة الأصل ومتقاطعة مع أسطوانة محورها هو المحور y، ويُعَد ذلك وصفًا للمزهرية في إطارها الخاص. يمكنك وصف موقع المزهرية بأنه "يبعد 20 سم تقريبًا من الحافة الأمامية للطاولة و30 سم من الحافة اليمنى"، واستخدمنا بذلك إطارًا آخر لوصف الموقع. يمكنك أيضًا التحدث عن كيفية مشاهدتك للطاولة من مسافة 2 متر تقريبًا وارتفاع 1.5 متر، ويستخدم ذلك إطارًا آخر لوصف مجال رؤيتك. يمكن أن تتجول أيضًا حول الطاولة معجبًا بالمزهرية الجميلة، وعندها يجب أن تستخدم إطارًا آخر لوصف مجال رؤيتك الجديد. لنفترض أنك خرجت للمشي، واقترب منك شخص غريب وسألك عن الاتجاهات إلى مكتب البريد، فما هو إطار الإحداثيات الذي ستستخدمه؟ هل ستستخدم خط العرض وخط الطول استنادًا إلى جرينتش في إنجلترا؟ أم ستستخدم عدد مباني المدينة على يسار ويمين موقعك الحالي؟ حسنًا، إن لم يكن لدى هذا الشخص الغريب جهاز استقبال GPS، فيُرجَّح أن يكون إطار إحداثيات مباني المدينة هو الأكثر فائدة. تمثيل الأشعة باستخدام المصفوفات العمودية لنعُد إلى عالمنا الافتراضي، حيث تكون الإزاحة من النقطة A إلى النقطة B هي الشعاع v، ولكن لا يعتمد الشعاع على أيّ نظام إحداثي. يمكن تمثيل الإزاحة باستخدام نظام الإحداثيات الأول (المخطط البياني الأوسط) بأنها: (الفرق في إحداثيات y, الفرق في إحداثيات x) وبالتالي تكون الإزاحة هي: (4, 0)T في هذا المخطط البياني، أي التحرّك بمقدار 4 وحدات من النقطة A باتجاه المحور x للوصول إلى النقطة B، بينما يُمثَّل الشعاع نفسه بالمصفوفة العمودية (2.8, -1.4)T في نظام الإحداثيات الثاني (الرسم البياني الأيمن). تُمثَّل كلٌّ من النقاط والأشعة باستخدام المصفوفات العمودية، فهل يُحتمَل أن يكون ذلك مربكًا؟ نعم هذا محتمل، إذ يؤدي هذا الارتباك إلى استخدام الإحداثيات المتجانسة Homogeneous Coordinates، والتي هي طريقة أخرى لتمثيل النقاط والأشعة باستخدام المصفوفات العمودية، ولكننا لن نشرحها في في هذا المقال. تمثيل جمع الأشعة باستخدام جمع المصفوفات العمودية إذا كانت الأشعة مُمثلَّة بالمصفوفات العمودية، فسنمثّل جمع الأشعة بجمع المصفوفات العمودية كما يلي: a = ( 3, 2 )T b = ( -2, 1 )T a + b = c = ( 1, 3 )T يوضح المخطط البياني السابق قاعدة الرأس إلى الذيل المُستخدَمة لجمع الشعاعين a و b للحصول على الشعاع c، وينتج عن جمع المصفوفتين العموديتين a و b المصفوفة العمودية c التي هي التمثيل الصحيح للشعاع c. تغيير ترتيب معاملات عملية الجمع مع الإجابة نفسها جرب حساب c = b + a للمصفوفتين العموديتين a و b السابقتين نفسهما، وستكون الإجابة نفسها هي: b + a = c = ( 1, 3 )T اتبع الخطوات التالية التي يكون فيها الشعاع b بمثابة شعاع البداية لإنشاء الرسم البياني التالي: ارسم الشعاع الأول b على شكل سهم، بحيث يكون ذيله عند نقطة الأصل. ارسم الشعاع الثاني a على شكل سهم، بحيث يكون ذيله عند نقطة رأس الشعاع الأول. يكون مجموعهما هو السهم المتشكّل من نقطة الأصل إلى رأس الشعاع الثاني. ملاحظة: تذكّر أن الأشعة ليس لها موقع محدّد، لذا يمكنك رسم مثيل للشعاع بحيث يبدأ ذيله من أيّ نقطة تريدها. يجب أن تكون نتيجة الجمع هي نفسها بما أن جمع الأشعة عملية تبديلية. تذكير: تُعَد عملية جمع الأشعة وجمع المصفوفات العمودية عملية تجميعية أيضًا. تدريب عملي ليكن لدينا الشعاعان: R = ( 4, 3 )T S = ( 1, 2 )T أوجد ناتج عملية الجمع: T = R + S يمكنك إيجاد الناتج من خلال استخدام جمع المصفوفات العمودية وقاعدة الرأس إلى الذيل، وسيكون الناتج عند تطبيق القاعدة هو: T = (5, 5)T ولنجمع الآن الشعاعين بترتيب معاكس كما يلي: T = S + R حيث يكون الشعاعان هما: S = ( 1, 2 )T R = ( 4, 3 )T وتبقى الإجابة نفسها: T = (5, 5)T استخدام متوازي الأضلاع في عملية جمع الأشعة هناك طريقتان لتشكيل عملية الجمع: T = S + R T = R + S حيث يكون الشعاعان هما: R = ( 4, 3 )T S = ( 1, 2 )T هناك طريقتان للرسم اعتمادًا على ذيل السهم الذي تضعه عند نقطة الأصل، بحيث إذا استخدمتَ كلا الطريقتين، فستحصل على متوازي أضلاع مع ناتج جمع الأشعة الذي يُمثَّل بسهم قطري يبدأ ذيله من نقطة الأصل. قد تتساءل عن ما هو متوازي الأضلاع Parallelogram وعمّا إذا كانت مادة الهندسة التي تعلّمتها في المدرسة الثانوية غامضة قليلًا، فمتوازي الأضلاع هو شكل رباعي فيه كل ضلعين متقابلين متوازيين ومتساويين في الطول، مثل السهمين الأزرقين الممثلين للشعاع s اللذين لهما الطول والاتجاه نفسه، والسهمين الأخضرين الممثلين للشعاع r اللذين لهما الطول والاتجاه نفسه. رحلة ثنائية الاتجاه أوجد ناتج ما يلي: w = u + v إذا كان: u = ( -3, 2 )T v = ( 1, -5 )T والجواب هو: w = ( -2, -3 )T يوضح المخطط البياني السابق عملية الجمع w= u + v. لنفترض أن نملةً ما تبدأ التحرك من نقطة الأصل وتسير على طول الشعاع u، ثم تسير على طول الشعاع v حتى تصل إلى رأس الشعاع w، وتبدأ النملة الثانية من نقطة الأصل وتسير على طول الشعاع w حتى تصل إلى نهايته، وبالتالي ستصل كلتا النملتين إلى النقطة نفسها. جمع الإزاحات وجمع الأطوال غير متساويين هل مشت النملتان المسافة نفسها في المثال السابق؟ في هذه الحالة، لا! فمن الواضح أن المشي في خط مستقيم إلى الوِجهة النهائية أقصر. لا يُعَد جمع الإزاحات (أو الأشعة) مثل جمع أطوالها، إذ ينتج عن جمع الشعاعين u و v شعاع أقصر من طول الشعاع u مع طول الشعاع v في المثال السابق؛ ويمكنك ملاحظة ذلك حاليًا من خلال النظر إلى المخطط البياني السابق، وتذكّر أن "الخط المستقيم هو المسافة الأقصر بين نقطتين" (سنناقش ذلك لاحقًا باستخدام صيغة فيثاغورس). تسمى هذه الحقيقة الرياضية بمتراجحة المثلث Triangle Inequality، وهي تنص على أن ناتج جمع أي طولين من أضلاع المثلث أكبر من طول الضلع الثالث. طول (v + u) <= طول (u) + طول (v) إليك حالة يكون فيها طول ناتج الجمع أقصر بكثير من مجموع الأطوال: e = ( 5, 4 )T g = ( -4.9, -3.9 )T e + g = (0 .1, 0.1 )T ملاحظة: نقلنا الشعاع g قليلًا من المكان الذي ينبغي أن يكون فيه للتوضيح. الأشعة ذات الاتجاه نفسه هل يمكنك التفكير في حالة يكون فيها طول ناتج الجمع مساويًا لمجموع طولي شعاعي الدخل؟ نعم يمكن ذلك إذا كان لأحد الشعاعين اتجاه الشعاع الآخر. يمكن توضيح هذه الحالة في المخطط البياني التالي، إذ تُستخدَم قاعدة الرأس إلى الذيل لجمع شعاعين يؤشّران إلى الاتجاه نفسه، وتُسمَّى الأشعة ذات الاتجاه نفسه بأنها مرتبطة خطية Collinear. نقلنا شعاع ناتج الجمع قليلًا عن مكانه الصحيح حتى تتمكّن من رؤيته. بعد ذلك جمعنا الشعاعين (4, 3)T و (2, 1.5)T، والناتج هو (6, 4.5)T. يمكنك حاليًا تحديد ما إذا كان هناك شعاعان لهما الاتجاه نفسه من خلال النظر إلى الصورة أو باستخدام المفاهيم الهندسية، وستتعرّف لاحقًا على إجراءٍ لاختبار ما إذا كان هناك شعاعان يؤشّران إلى الاتجاه نفسه. يعبِّر هذا المثال عن حالةٍ تطبِّق إشارة المساواة "=" الموجودة في الصيغة التالية: طول (v + u) <= طول (u) + طول (v) جمع الأشعة الصفرية هل يمكنك التفكير في حالةٍ أخرى تطبّق إشارة المساواة "=" الموجودة في الصيغة السابقة؟ نعم، عندما يكون أحد الشعاعين شعاعًا صفريًا كما يلي: طول (0 + u) <= طول (u) + طول (0) اجمع المصفوفة الصفرية ( 0, 0 )T مع المصفوفة ( 4, 3 )T، فإذا مثّلت المضفوفة ( 0, 0 )T شعاع إزاحة، فهذا يعني عدم تطبيق أيّ شيء لتغيير موضع الشعاع الآخر. جمع النقاط والأشعة لنفترض أنك تريد جمع الشعاع (1, 2)T والنقطة (4, 4)T. إذا كان ذلك منطقيًا، فأجرِ هذه العملية، وما هو نوع الكائن الناتج؟ يكون الناتج نقطة كما يلي: (4, 4)T + (1, 2)T = ( 5, 6 )T إذا كان الشعاع (1, 2)T إزاحة (أي مقدار يمكن تغيير قيمة x به، ومقدار يمكن تغيير قيمة y به)، فيجب أن تكون النتيجة نقطة في موقع جديد كما يلي: (4, 4)T + (1, 2)T = ( 5, 6 )T هذه إحدى الحالات التي يكون فيها استخدام التمثيل (أي المصفوفات العمودية) نفسه لكل من النقاط والأشعة أمرًا مربكًا، لذا يجب عليك تتبّع نوع الكائن الذي تمثله كل مصفوفة. تنتج نقطةٌ عن جمع شعاع إزاحة ونقطة كما في المخطط البياني التالي: يُعَد جمع كائنين رياضيين من نوعين مختلفين غريبًا قليلًا، ولا تنسَ أن إشارة الجمع "+" إشارة قابلة للتحميل الزائد overloaded. الانسحاب Translation تُسمَّى عملية جمع شعاع ونقطة بالانسحاب Translation، ويُقال أحيانًا أن النقطة الأصلية مسحوبةً إلى موقع جديد. قد تكون المصطلحات هنا مربكةً قليلًا، إذ ينتج عن جمع شعاع ونقطة من الناحية الرياضية نقطةٌ جديدة، بحيث تبقى النقطة الأولى دون تغيير، ولكن من الجيد التفكير في الصور "التي تتحرك عبر الشاشة" عند جمع أشعة الإزاحة والنقاط الخاصة بها في مصطلحات الرسوميات الحاسوبية، لذا قد تسمع أحيانًا مصطلحات مثل "تحريك نقطة" و"النقطة القديمة" و"النقطة الجديدة". لقد رأينا تطبيق العمليات التالية: شعاع + شعاع. نقطة + شعاع. نقطة - نقطة. فهل تفترض أنه يمكن إجراء العملية نقطة + نقطة؟ لا يُعَد جمع نقطتين شيئًا هندسيًا. يمكنك تشكيل مجموع شعاعين عموديين يمثلان نقاطًا ميكانيكيًا، ولكن النتيجة لا تكون ذات معنى فعلي. لقد وصلنا إلى نهاية هذا المقال، وسنناقش في المقال التالي العمليات على الأشعة بصورة أكبر، وسنناقش لاحقًا مزيدًا من العمليات على الأشعة. ترجمة -وبتصرُّف- للفصل Vector Addition من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: كيفية جمع وطرح المصفوفات العمودية والمصفوفات السطرية الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد التعرف على النقاط والخطوط في الرسوميات الحاسوبية ثلاثية الأبعاد
-
سنناقش في هذا المقال كيفية جمع وطرح المصفوفات العمودية والسطرية ولكن لنتعرّف على بعض المصطلحات التي سنستخدمها: النقطة Point: كائن هندسي، وهو موقع في فضاء ثلاثي الأبعاد أو ثنائي الأبعاد. الشعاع Vector: كائن هندسي له خصائص الاتجاه والطول، ولكن ليس لديه خاصية الموقع. المصفوفة العمودية Column Matrix: قائمة مرتبة من الأعداد تُرتَّب ضمن عمود. المصفوفة السطرية Row Matrix: قائمة مرتبة من الأعداد تُرتَّب ضمن صف. العنصر Element: أحد الأعداد التي تشكل المصفوفة العمودية أو السطرية. البعد Dimension: عدد العناصر في مصفوفة عمودية أو سطرية. الإزاحة Displacement: هو الفرق بين موقعين، ويعبَّر عنه باستخدام شعاع. كما سنوضّح في هذا المقال المواضيع التالية: منقول Transpose المصفوفة العمودية أو السطرية. جمع المصفوفات العمودية والمصفوفات السطرية. الخاصية التبديلية لجمع المصفوفات. الخاصية التجميعية لجمع المصفوفات. طرح المصفوفات العمودية والمصفوفات السطرية. الخاصية غير التبديلية لطرح المصفوفات. منقول مصفوفة Transposition ماذا تُسمَّى عملية قَلب مصفوفة العمودية، أو ماذا تُسمَّى عملية قَلب مصفوفة سطرية إلى مصفوفة عمودية؟ تسمّى عملية المنقول Transposition. تُعَد المصفوفة السطرية والمصفوفة العمودية من نوعين مختلفين حتى لو كان لهما البعد نفسه وتحتويان على العناصر نفسها، لذا يُستخدَم الحرف العُلوي "T" عند كتابة مصفوفة عمودية مثل صفٍ من الأعداد، إذ يكون العنصر الأول في المصفوفة العمودية هو العنصر الموجود أعلى العمود، ويكون العنصر الأول في المصفوفة السطرية هو العنصر الموجود على يسار الصف. سنوضح فيما يلي طريقتين لكتابة المصفوفة العمودية نفسها، إذ تُكتَب عناصر المصفوفة العمودية بالطريقة نفسها بغض النظر عن كيفية عرض المصفوفة العمودية. يعني وجودُ حرف T العُلوي في المصفوفة العمودية قلبَ العمود إلى صف، وينتج عنه مصفوفة سطرية، وتستخدم بعض الكتب حرف t صغير. ما هو : (1, 2, 3)T T؟ ( 1, 2, 3 )T T = ( 1, 2, 3 ) يؤدي تطبيق المنقول مرتين إلى العودة إلى ما بدأت به. يبدو هذا واضحًا الآن، ولكن قد يكون من المفيد تذكّر ذلك لاحقًا عند تطبيق المعالجة الجبرية. جمع المصفوفات العمودية يمكن جمع مصفوفتين عموديتين لهما البُعد نفسه، وتنتج مصفوفة عمودية لها البعد نفسه أيضًا، وينطبق الشيء نفسه على المصفوفات السطرية. تُطبَّق عملية الجمع من خلال جمع العناصر المتقابلة لمصفوفات الدخل لإنتاج العناصر المقابلة لمصفوفة الخرج كما يلي: ( 42, -12 )T + ( 8, 24 )T = ( 50, 12 )T ( 32.98, -24.71, 9.392 )T + ( -32.98, +24.71, -9.392 )T = (0, 0, 0)T ( 9.2, -8.6, 3.21, 48.7 ) + ( -2.1, 4.3, 1.0, 2.3 ) = ( 7.1, -4.3, 4.21, 51.0 ) ( 1, 2, 3 ) + ( 10, 20, 30 ) = ( 11, 22, 33 ) إذا كان a و b مصفوفتين من النوع نفسه، فإن a + b = c يعني أن كل عنصر ci = ai + bi أوجد ناتج ما يلي: ( 2, -2 )T + ( 8, 6 )T الجواب: ( 2, -2 )T + ( 8, 6 )T = ( 10, 4 )T جمع المصفوفات ثلاثية الأبعاد يجب أن تكون المصفوفات التي تجمعها لها البُعد والنوع نفسه سواءً كانت عمودية أم سطرية. يجري التعامل مع كل بُعد بصورة مستقلة عن الأبعاد الأخرى عند جمع المصفوفات، فمثلًا سيكون لديك ثلاث عمليات جمع منفصلة باستخدام العمليات الحسابية العادية عند جمع مصفوفتين عموديتين ثلاثيتي الأبعاد، إذ تُجمَع العناصر الأولى من المصفوفات التي تمثل معاملات عملية الجمع لتكوين العنصر الأول من المصفوفة الناتجة. أوجد ناتج ما يلي: ( 8, 4, 6 )T + ( 2, -2, 9 )T ( 2, -2, 9 )T + ( 8, 4, 6 )T الجواب: ( 8, 4, 6 )T + ( 2, -2, 9 )T = ( 10, 2, 15 )T ( 2, -2, 9 )T + ( 8, 4, 6 )T = ( 10, 2, 15 )T الخاصية التبديلية Commutative لجمع المصفوفات تُعَد عملية جمع المصفوفات عملية تبديلية، أي أن: a + b = b + a، وينطبق ذلك على المصفوفات السطرية والعمودية بجميع أبعادها، إذ يمكن القول أن: a + b + c = b + c + a = c + a + b = …... تعني الخاصية التبديلية أن ترتيب جمع المصفوفات غير مهم، إذ تعمل المصفوفات باستخدام طريقة عمل الأعداد نفسها بالنسبة للجمع: 1 + 2 = 2 + 1، فالمصفوفات والأعداد عادةً لا تعمل بالطريقة نفسها ولكنها كذلك بالنسبة لعملية الجمع. أوجد ناتج ما يلي: ( -1, -2, 3 )T + ( 1, 2, -3 )T الجواب: ( -1, -2, 3 )T + ( 1, 2, -3 )T = ( 0, 0, 0 )T المصفوفة العمودية الصفرية ليكن لدينا مثلًا مصفوفة صفرية كما يلي: ( 73.6, -41.4 )T + ( 0.0, 0.0 )T = (73.6, -41.4 )T تُسمَّى المصفوفة التي تكون جميع عناصرها أصفارًا بالمصفوفة الصفرية Zero Matrix، ويكون ناتج جمع المصفوفة الصفرية والمصفوفة a لهما النوع نفسه هو المصفوفة a. نرمّز المصفوفة الصفرية بالرمز 0 (صفر بالخط العريض) الذي يختلف عن 0 (العدد الحقيقي صفر)، ويحدّد السياق الذي ترى فيه الرمز 0 ما إذا كانت مصفوفة صفرية سطرية أم عمودية وعدد العناصر التي تحتوي عليها. فإذا رأيت (73.6, -41.4 )T + 0 ، فيمكنك أن تفترض ما هو النوع الصحيح للمصفوفة 0 والمناسب للتعبير. الخاصية التجميعية Associative لجمع المصفوفات أوجد ناتج ما يلي: ( 2, 5, -9 ) + ( -32.034, 94.79, 201.062 ) + ( -2, -5, 9 ) الجواب: ( 2, 5, -9 ) + ( -32.034, 94.79, 201.062 ) + ( -2, -5, 9 ) = ( -32.034, 94.79, 201.062 ) + ( -2, -5, 9 ) + ( 2, 5, -9 ) = ---------------------------- ( -32.034, 94.79, 201.062 ) + ( 0, 0, 0) = ( -32.034, 94.79, 201.062 ) قد يفقد بعض الطلاب صبرهم عندما يجمّعون المصفوفتين الأوليتين، ثم يجمّعون الناتج مع المصفوفة الثالثة، ولكن يدرك بعضهم أنه يمكن إعادة ترتيب المصفوفات إلى ترتيب أكثر ملاءمة بما أن جمع المصفوفات يُعَد عملية تبديلية، إذ يمكنهم اختيار عملية الجمع "+" التي يريدون تطبيقها أولًا بعد أن تصبح المصفوفات في ترتيب مناسب. يُعَدّ جمع المصفوفات عملية تجميعية، وهذا يعني أن: ( a + b ) + c = a + ( b + c ) أي يمكن جمع المصفوفتين a و b ثم جمع النتيجة مع المصفوفة c، وستكون النتيجة النهائية هي نفسها كما لو جمعت المصفوفة a إلى نتيجة جمع المصفوفتين b و c. يمكن تطبيق ذلك على المصفوفات السطرية والعمودية بجميع أبعادها. هل يمكنك إيجاد ناتج ما يلي في الوقت المناسب؟ ( 25.1, -19.6 ) + ( -5.0, 9.0 ) + ( 12.4, 8.92 ) + ( -20.1, 10.6 ) الجواب: ( 25.1, -19.6 ) + ( -5.0, 9.0 ) + ( 12.4, 8.92 ) + ( -20.1, 10.6 ) = ------------- --------------- \ / \ / \ / \ / ( 25.1, -19.6 ) + ( 12.4, 8.92 ) + ( -25.1, 19.6 ) --------------- --------------- \ / \ / \ / \ / \ / ( 0.0, 0.0 ) + ( 12.4, 8.92 ) = ( 12.4, 8.92 ) بعض الأشياء التي لا يمكن استخدامها عند جمع المصفوفات يعني الرمز "+" زيادة التحميل Overloaded في مصطلحات علوم الحاسوب، مما يعني أن العملية المُستدعاة تعتمد على نوع المعاملات، فمثلًا: يعني الرمز + هنا جمع أعداد حقيقية: 1.34 + -9.06 يعني الرمز + هنا جمع مصفوفات عمودية: ( 84.02, 90.31 )T + ( -14.23, 10.85 )T بينما لا تعطي الأمور التالية أيّ معنًى: لا يمكن جمع عدد ومصفوفة: 34.5 + ( 84.02, 90.31 )T لا يمكن جمع مصفوفة سطرية ومصفوفة عمودية: ( 84.02, 90.31 ) + ( -14.23, 10.85 )T * لا يمكن جمع مصفوفات ذات أبعاد مختلفة: ( 84.02, 90.31 ) + ( -14.23, 10.85, 32.75 ) تكون هذه المشاكل واضحة عند كتابة العناصر كما سبق، ولكنها تكون أقل وضوحًا عند استخدام الرموز المتغيرة كما يلي: لا يمكن جمع عدد ومصفوفة a + x. يجب التأكد من أن المعاملين x + y من النوع نفسه. يمكن إجراء عملية الجمع: ( 4.5, x1, w ) + ( -2.3, 3, y2 ) كما يلي: ( 4.5, x1, w ) + ( -2.3, 3, y2 ) = ( 2.2, x1 +3, w+ y2 ) طرح المصفوفات يمكن طرح مصفوفتين من النوع نفسه وتنتج مصفوفة ثالثة من ذات النوع، إذ يمكن طرح مصفوفتين من خلال طرح العناصر المتقابلة، مع الحرص على إبقاء العناصر بالترتيب نفسه كما يلي: (10, 8, 12 ) - (2, 14, 9 ) = ( 10 - 2, 8 - 14, 12 - 9 ) = ( 8, -6, 3 ) إذا كانت a و b مصفوفتان من النوع نفسه، فإن a - b = c تعني أن كل عنصر ci = ai - bi ليست عملية الطرح تبديلية أوجد ناتج ما يلي: (22, 5, -12 ) - (10, -5, 3 ) (10, -5, 3 ) - (22, 5, -12 ) الجواب: (22, 5, -12 ) - (10, -5, 3 ) = ( 22 - 10, 5 - (-5), -12 - 3 ) = ( 12, 10, -15 ) (10, -5, 3 ) - (22, 5, -12 ) = ( 10 - 22, (-5) - 5, 3 - (-12) ) = (-12, -10, 15 ) حقيقة رياضية: لا تُعَد عملية الطرح تبديلية، وهذا يعني أنه لا يمكنك تغيير الترتيب عند طرح المصفوفتين a - b. يمكن إيجاد معاكس مصفوفة -a من خلال إيجاد معاكس كل عنصر من عناصر هذه المصفوفة كما يلي: -(22, 5, -12 ) = ( -22, -5, 12 ) -( 19.2, 28.6, 0.0 )T = ( -19.2, -28.6, 0.0 )T يمكن القول أنه إذا كانت a هي ( a0, a1, …, a2) فإن -a تعني (-a0, -a1, …, -a2). إدخال الإشارة السالبة داخل الأقواس أوجد ناتج عملية الطرح التالية: ( -7.2, -98.6, 0.0 )T - ( -2.2, -2.4, 3.0 )T الجواب: ( -7.2, -98.6, 0.0 )T - ( -2.2, -2.4, 3.0 )T = ( -7.2 -(-2.2), -98.6 -(-2.4), 0.0 - 3.0)T = ( -5.0, -96.2, - 3.0)T ولكن قد تجد أن الطريقة التالية أسهل للحل: ( -7.2, -98.6, 0.0 )T - ( -2.2, -2.4, 3.0 )T = ( -7.2, -98.6, 0.0 )T + ( +2.2, +2.4, -3.0 )T = ( -5.0, -96.2, - 3.0)T اُستخدِمت الإشارة السالبة الخارجية لعكس إشارة المصفوفة الثانية، ثم تُجمَع المصفوفتان الناتجتان كما يلي: a - b = a + (-b) قد يكون من المفيد تذكّر ذلك، حيث يمكنك عكس إشارة المصفوفات بحيث تكون العملية هي جمع المصفوفات ثم إعادة ترتيب عملية الجمع لأن عملية الجمع تُعَد عملية تبديلية: a - b + c - d = a + (-b) + c + (-d) = (-d) + a + c + (-b) = الجمع بأيّ ترتيب تريده إليك بعض الحقائق الأخرى: a + (-a) = 0 a - a = 0 لاحظ أن 0 هي المصفوفة الصفرية، ولها نوع المصفوفة a نفسه ولكن تكون جميع عناصرها صفر. أجرِ العملية التالية: ( 4, -5, 6.2 ) + ( -43.132, 13.6, 86.5 ) - ( 4, -5, -4.8 ) حاول إعادة الترتيب بدلًا من التسرع والحساب مباشرةً: ( 4, -5, 6.2 ) + ( -43.132, 13.6, 86.5 ) - ( 4, -5, -4.8 ) = ( 4, -5, 6.2 ) + ( -43.132, 13.6, 86.5 ) + ( -4, 5, 4.8 ) = ( 4, -5, 6.2 ) + ( -4, 5, 4.8 ) + ( -43.132, 13.6, 86.5 ) = ( 0, 0, 11 ) + ( -43.132, 13.6, 86.5 ) = (-43.132, 13.6, 97.5 ) وقد تتخطى بعض الخطوات إذا حاولت الحساب ذهنيًا. تدريب عملي أولًا، ما هو مجموع الإزاحات الثلاثة التالية: d = ( -12.4, 14.8, 0.0 )T e = ( 6.2, -10.2, 17.0 )T f= ( 6.2, -4.6, -17.0 )T والناتج هو: (0.0, 0.0 0.0)T. ثانيًا، أوجد قيمة x و y و z بحيث يكون ما يلي صحيحًا: a = ( 8.6, 7.4, 3.9 ) b = ( 4.2, 2.2, -3.0 ) c = ( x, y, z ) a + b + c = 0 وبالتالي يمكن كتابة ما يلي: a + b + c = ( 12.8, 9.6, 0.9 ) + (x, y, z) = ( 12.8+x, 9.6+y, 0.9+z ) = (0, 0, 0) لذا يجب أن يكون: 12.8 + x = 0; x = -12.8 9.6 + y = 0; y = -9.6 0.9 + z = 0; z = -0.9 استخدام الجبر في جمع وطرح المصفوفات كان التمرين السابق هو العثور على عناصر المصفوفة c عندما: a** + b + c = 0**، ولكن إن لم تكن على علمٍ أنها مصفوفات، فقد تميل إلى حل هذا التمرين باستخدام جبر الأعداد الحقيقية real كما يلي: a + b + c = 0 a + b = -c + 0 (a + b) = -c -(a + b) = c يمكن استخدام هذه الطريقة طالما أن المصفوفات من النوع نفسه، والعمليات هي جمع "+" أو طرح "-" فقط، وبالتالي يمكنك التظاهر بأنك تطبّق عمليات جبرية عادية. لاحظ أن المعادلة الأخيرة تعني "اجمع a مع b، ثم اعكس إشارة النتيجة لتحصل على c". لنتعمق أكثر ولننظر فقط إلى العناصر الأولى من المصفوفات: ( a0, … ) + ( b0, … ) + ( c0, … ) = ( 0, …) ( a0, … ) + ( b0, … ) = -( c0, … ) + ( 0, …) ( a0, … ) + ( b0, … ) = -(c0, … ) ( a0 + b0, … ) = -( c0, … ) -( a0 + b0, … ) = ( c0, … ) -( a0 + b0) = c0 إذا كان ذلك غير منظم بالنسبة لك، فامسح ذهنيًا بعض الأشياء غير المرغوب فيها كما يلي: a0 + b0 + c0 = 0 a0 + b0 = -c0 + 0 a0 + b0 = -c0 a0 + b0 = -c0 -( a0 + b0) = c0 وتتبع العناصر الأخرى النمط نفسه، وبالتالي تكون النتيجة صحيحة بالنسبة للمصفوفة ككل. أوجد قيمة c1 و c0 بحيث يكون ما يلي صحيحًا: a = ( -4, 2 )T b = ( 8, 3 )T c = ( c0, c1 )T a + b + c = 0 ويكون الحل باستخدام الطريقة التالية: a + b + c = 0 a + b = -c ( 4, 5 )T = -( c0, c1 )T -( 4, 5 )T = ( c0, c1 )T (-4, -5 )T = ( c0, c1 )T وبالتالي: c0 = -4 c1 = -5 وصلنا إلى نهاية المقال الذي تعلّمنا من خلاله كيفية جمع وطرح المصفوفات وسنوظّف هذه المعرفة في المقال التالي لتعلّم كيفية جمع [الأشعة](رابط المقال التالي). ترجمة -وبتصرُّف- للفصل Matrix Addition من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ المزيد المقال السابق الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد. أنواع البيانات والعمليات الأساسية في لغة بايثون.
-
سنناقش في هذا المقال كائنات الرسوميات الحاسوبية التي هي الأشعة والنقاط وكيفية تمثيلها في الحاسوب بوصفها مصفوفات عمودية، فالمصفوفة العمودية column matrix هي كائن رياضي له العديد من الاستخدامات إلى جانب استخدامه في الرسوميات الحاسوبية، ولكن سنتعرف في هذا المقال على كيفية استخدامها في الرسوميات الحاسوبية فقط. سنتعرّف في هذا المقال على المواضيع التالية: نمذجة وعرض الرسوميات الحاسوبية. النقاط والأشعة الهندسية. المصفوفات العمودية والسطرية. حساب الإزاحات. مساواة المصفوفات العمودية. أسماء المصفوفات العمودية وعناصرها. تمثيل النقاط باستخدام المصفوفات العمودية. تُعَد النقاط والأشعة الهندسية ضروريةً في الرسوميات الحاسوبية، ويجب تمثيلها بطريقة تسهّل التعامل معها، لذا تُعَد المصفوفة العمودية الخيار المعتاد لذلك. تستخدم بعض كتب الرسوميات مصطلح الشعاع العمودي Column Vector للتعبير عن الكائن الذي نسميه في مقالنا بالمصفوفة العمودية، إذ يُعَد ذلك مجرد اختلافٍ في المصطلحات ولا يؤثر على المفاهيم أو الصيغ التي سنوضّحها لاحقًا، ولكن المشكلة الأسوأ هي أن بعض الكتب تستخدم المصفوفات السطرية Row Matrices، فالمصفوفات العمودية والمصفوفات السطرية متكافئة، ولكن المعادلات المكتوبة باستخدام المصفوفات السطرية ليست هي نفسها المكتوبة باستخدام المصفوفات العمودية. يمكننا ضبط هذه الاختلافات، ولكنها مزعجة قليلًا. مثال مشهد السائح الافتراضي ما هما نوعا الكائنات الهندسية التي يمكن تمثيلها باستخدام المصفوفات العمودية؟ هما النقاط والأشعة. تتكون الرسوميات الحاسوبية من نشاطين: أولهما إنشاء عالم خيالي ثلاثي الأبعاد في الحاسوب، وثانيهما إنتاج صور ثنائية الأبعاد لذلك العالم من نقاط نظر مختلفة. يشبه برنامج الرسوميات السائحَ الذي يتجول في معالم طبيعية رائعة ويلتقط الصور بالكاميرا، ويُجهَّز السائح الافتراضي بكاميرا سينمائية مع الرسوم المتحركة الحاسوبية. يتكون المشهد الخيالي من كائنات في فضاء ثلاثي الأبعاد، ويُحدَّد كل كائن باستخدام النقاط والخطوط التي تقع على سطحه مثل المشهد الشتوي السابق لصاحبه توان فان Tuan Phan. يتكون هذا النموذج من نقاط وقطع مستقيمة تربط بينها، وهناك حاجة إلى مزيدٍ من العمل لملء المنطقة بين القطع المستقيمة ولتطبيق الإضاءة للحصول على صورة واقعية. صورة المشهد الكاملة هل يمكن تمثيل الكرة باستخدام النقاط والخطوط؟ نعم، بالتأكيد. انظر إلى جسد رجل الثلج، إذ يبدو الشكل وكأنه كرة عند ملء المضلعات. هذه هي الصورة الكاملة، مع ملء المضلعات وتطبيق الإضاءة وإلقاء قليلٍ من ندف الثلج. يمكنك طلب نماذج من الكرات والأسطوانات والأقماع ووضعها في مشهدك في المكان الذي تريده باستخدام حزم الرسوميات الحاسوبية ثلاثية الأبعاد مثل OpenGL التي تملأ أيضًا المضلعات تلقائيًا وتطبّق نموذج الإضاءة لإنتاج تظليل واقعي. مشهد افتراضي آخر هل يمكن لسائحنا الافتراضي أن يتجول في مشهد الثلوج الافتراضي ويلتقط صورة مختلفة؟ نعم، يمكنه ذلك. يضع نموذج المشهد الثلجي كائنات مختلفة في فضاء ثلاثي الأبعاد، وبالتالي يمكن لسائحنا الافتراضي أن يتجول على يمين رجل الثلج ويلتقط صورةً أخرى ثنائية الأبعاد باستخدام الكاميرا الافتراضية، إذ أُنتِجت الصورة السابقة باستخدام النموذج والإضاءة نفسها، ولكن مع وضعٍ مختلف للكاميرا. الشعاع Vector ما هي النقطة؟ وضحنا في المقال السابق مفهوم النقطة بأنها موقع في الفضاء، حيث تُعَد النقطة في الهندسة موقعًا في الفضاء فليس لها حجم وخاصيتها الوحيدة هي الموقع، بينما تكون النقطة عادةً في الرسوميات الحاسوبية هي رأس الشكل ثلاثي الأبعاد. أما الشعاع الهندسي فله خاصيتان: الطول والاتجاه، ولكن ليس له موقع ثابت في الفضاء، فإذا كان له موقع ثابت، فسيكون قطعة مستقيمة. قد يكون الحديث عن شيء ليس له موقع أمرًا غريبًا، ولكنه يسهّل تطبيق الرسوميات الحاسوبية ثلاثية الأبعاد. يسمى هذا المزيج من "المسافة والاتجاه" بالإزاحة Displacement أحيانًا، ويمكن تطبيق الإزاحة نفسها (أي إزاحة واحدة فقط) على كل نقطة من النقاط المختلفة. لنفترض أن لدينا المكعب التالي مثلًا، إذ يحتوي الوجه الأمامي على أربعة رؤوس (أربع نقاط). إذا تحركتَ من كل نقطة من هذه النقاط بمقدار المسافة والاتجاه نفسه، فستصل إلى النقاط الأربعة للوجه الخلفي. تمثل العبارة "المسافة والاتجاه نفسه" الشعاعَ الذي يظهر بوصفه خطًا مع رأس سهم، ويوضح الشكل التالي هذا الشعاع أربع مرات، مرة واحدة لكل نقطة من الوجه. المصفوفة العمودية لنفترض أنك قضيت عطلة الربيع على الشاطئ والشمس مشرقة، فهل يسطع ضوء الشمس في الاتجاه نفسه لكل الموجودين على الشاطئ؟ نعم، إذ يكون "الاتجاه نحو الشمس" نفسه لكل الموجودين على الشاطئ، وبالتالي فهو شعاع. نهتم أحيانًا بالاتجاه فقط دون الاهتمام بالموقع أو بالطول كما في السؤال السابق، لذا نستخدم الشعاع لهذا الغرض، فطوله ليس مهمًا، لأننا نستخدم طولًا واحدًا في كثير من الأحيان. نهتم في أحيانٍ أخرى بكلٍ من الاتجاه والحجم، وعندها يُستخدَم كلٌّ من اتجاه وطول الشعاع. يمكن تمثيل الشعاع بقائمةٍ من الأعداد تدعى المصفوفة العمودية، والتي هي قائمة مرتبة من الأعداد المكتوبة ضمن عمود. تستخدم بعض الكتب كلمة "شعاع" لتعبّر عن فكرة الشعاع وتمثيله بوصفه ترتيبًا لثلاثة أعداد، ولكن يمكن أن يكون ذلك مربكًا أحيانًا. إليك فيما يلي مثالًا لمصفوفة عمودية: يسمى كل عددٍ في المصفوفة العمودية عنصرًا Element، وتكون هذه الأعداد حقيقية real، ويسمى عدد العناصر في الشعاع بالبُعد Dimension. المصفوفة السطرية Row Matrix هي قائمة مرتبة من الأعداد المكتوبة في صف واحد مثل المصفوفة السطرية: (12.5, -9.34). سنمثّل الأشعة دائمًا باستخدام مصفوفات عمودية بهدف التناسق، وتمثل بعض الكتب الأشعة باستخدام مصفوفات سطرية، إذ لا يحدث ذلك فرقًا جوهريًا، ولكنه يغيّر بعض الصيغ الرياضية قليلًا. ما هو عدد العناصر الموجودة في كل مصفوفة عمودية؟ الجواب هو: استخدام المتغيرات بوصفها عناصر مصفوفة يمكن أن تكون عناصر المصفوفة العمودية متغيرات variables كما يلي: يُعطَى العنصر الأول في المصفوفة العمودية أحيانًا الفهرس "0" أوالفهرس "1" أحيانًا. العرض المناسب للمصفوفة العمودية هل المصفوفة العمودية التالية: هي المصفوفة العمودية التالية نفسها؟ لا يمكن ذلك، فالمصفوفة العمودية هي قائمة مرتبة من الأعداد، وهذا يعني أن كل موضع في المصفوفة العمودية يحتوي على عدد أو متغير معين. يُعَد مظهر المصفوفات العمودية غريبًا في النصوص المطبوعة، لذا من الشائع كتابتها كما يلي: (2.9, -4.6, 0.0) T يرمز الحرف "T" إلى منقول المصفوفة Transpose الذي يعني تحويل الصفوف إلى أعمدة (سنوضح لاحقًا بالتفصيل معنى المنقول). المساواة بين المصفوفات هل المصفوفة (1.2, -3.9, 0.0) تساوي المصفوفة (1.2, -3.9, 0.0)T؟ لا، فالمصفوفة الأولى هي مصفوفة سطرية، والمصفوفة الثانية هي مصفوفة عمودية، أي مكتوبة ضمن صف، ولكن الحرف "T" يعني أنها مصفوفة عمودية، إذ تُعَد المصفوفات السطرية والمصفوفات العمودية أنواعًا مختلفة من الكائنات، ولا يمكن أن تكون متساوية. ربما لم نميّز سابقًا بين المصفوفات السطرية والمصفوفات العمودية، ولم نوضِّح اختلاف الأشعة الهندسية عن المصفوفات العمودية المستخدمة لتمثيل هذه الأشعة، إذ يمكن أن تبدو هذه الاختلافات صعبة حاليًا، ولكن أبقِها حاضرةً في ذهنك لتجنب الالتباس مستقبلًا. يمكن أن تكون المصفوفتان العموديتان متساويتين إذا كانت: كلتا المصفوفتين مصفوفةً عمودية. لكل منهما البعد (عدد العناصر) نفسه. العناصر المقابلة لبعضها البعض في المصفوفتين متساوية. يمكن أن تكون المصفوفتان السطريتان متساويتين إذا كان: كلتا المصفوفتين مصفوفات سطرية. لكل منهما البعد (عدد العناصر) نفسه. العناصر المقابلة لبعضها البعض في المصفوفتين متساوية. الفروق الدقيقة بين المصفوفات هل المصفوفتان السطريتان (8.0, -1.63, 7.0, 0.0) و (8.0, -1.63, 7.0, 1.0) متساويتان؟ لا. يمكن مقارنة المصفوفات من النوع نفسه فقط، إذ يمكنك المقارنة بين مصفوفتين عموديتين ثلاثيتي الأبعاد، أو مصفوفتين سطريتين رباعيتي الأبعاد مثلًا، فليس من المنطقي مقارنة مصفوفة سطرية ثلاثية الأبعاد مع مصفوفة عمودية ثلاثية الأبعاد. ( 6, 8, 12, -3 )T = ( 6, 8, 12, -3)T ( 6, 8, 12, -3 ) = ( 6, 8, 12, -3 ) ( 6, 8, 12, -3 ) ≠ ( -2.3, 8, 12, -3 ) ( 6, 8, 12, -3 )T ≠ ( 6, 8, 12, -3 ) ( 6, 8, 12, -3 )T ≠ ( 6, 8, 12 )T يمثل المحرف ≠ عدم المساواة (قد يكون من الصعب رؤيته باستخدام متصفح الويب). تكون القواعد أحيانًا مريحة وتؤدي إلى عدم الدقة في التمييز بين المصفوفات السطرية والمصفوفات العمودية، ولكن سيؤدي إبقاء التمييز بينها واضحًا إلى تجنب الارتباك مستقبلًا. هل تُعَد المصفوفتان العموديتان (1.53, -0.03, 9.03, 0.0, +8.64)T و (1.53, -0.03, 9.03, 1.0, -8.64)T متساويتين؟ لا، ليستا متساويتين. أسماء المصفوفات من المفيد أن يكون للمصفوفات أسماء، إذ يُستخدَم عادةً حرف صغير بخط عريض لمصفوفة سطرية أو عمودية كما يلي: a = ( 1.2, -3.6 ) x = ( x1, x2, x3, x4 ) r = ( r0, r1 )T من المعتاد استخدام الأسماء من بداية أحرف الأبجدية الإنجليزية للمصفوفات العمودية التي تكون عناصرها معروفة مثل المصفوفة a السابقة، واستخدام الأسماء من نهاية أحرف الأبجدية الإنجليزية للمصفوفات العمودية التي تكون عناصرها متغيرات. تكون أسماء عناصر المصفوفة العمودية غالبًا مؤلفة من اسم المصفوفة العمودية الكاملة مع رمز سفلي مثل المصفوفة العمودية r وعناصرها r0 وr1. من الصعب كتابة الأحرف ذات الخط العريض بالقلم الرصاص أو بالطباشير، لذا بدلًا من ذلك يُوضَع سهم أو شريط فوق اسم المصفوفة العمودية كما يلي: _ -> x x لنفترض أن لدينا ما يلي: x = ( x1, x2 ) y = ( 3.2, -8.6 ) x = y ما الذي يجب أن يكون صحيحًا بشأن x1 و x2؟ x1 = 3.2 و x2 = -8.6 تمثيل الأشعة باستخدام المصفوفات العمودية تُستخدَم المصفوفات العمودية لتمثيل الأشعة وتُستخدَم لتمثيل النقاط أيضًا. يُستخدَم في الفضاء ثنائي الأبعاد نوع البيانات نفسه -وتكون مصفوفات عمودية ثنائية الأبعاد- لتمثيل نوعين مختلفين من الكائنات الهندسية هما النقاط والأشعة، ويُعَد ذلك أمرًا مربكًا، لذا سنصحح هذا الوضع لاحقًا. يوضح الشكل السابق شعاع الإزاحة الذي يمثل الفرق بين نقطتين في المستوي x-y، إذ نستخدم حاليًا أمثلةً في فضاء ثنائي الأبعاد، وسيأتي الفضاء ثلاثي الأبعاد لاحقًا. النقطة A لها الإحداثيات: x=2 و y=1، ويمكن تمثيلها بمصفوفة عمودية: (2, 1)T. النقطة B لها الإحداثيات: x=7 و y=3، ويمكن تمثيلها بمصفوفة عمودية: (7, 3)T. يمكن حساب الإزاحة من النقطة A إلى النقطة B في مسألتين منفصلتين هما: الإزاحة x هي الفرق بين قيم X أي: 7-2 = 5. الإزاحة y هي الفرق بين قيم Y أي: 3-1 = 2. وشعاع الإزاحة المُعبَّر عنه بمصفوفة هو: (5, 2)T = d. تُصوَّر أشعة الإزاحة على شكل سهم يربط بين نقطتين، إذ تكون النقطة A هي ذيل الشعاع، والنقطة B هي رأس الشعاع في الرسم البياني، ولكن تذكّر أن الأشعة ليس لها موضع، لذا يُعَد هذا الرسم مجرد مكان مناسب لرسمها، ولا يعبّر عن موضعها. الإزاحة Displacement تمثل المصفوفة العمودية d شعاع الإزاحة من النقطة A إلى النقطة B، فما هي المصفوفة العمودية التي تمثل الإزاحة من النقطة B إلى النقطة A؟ الإزاحة من النقطة B إلى النقطة A هي: الإزاحة x هي: 2-7 = -5 الإزاحة y هي: 1-3 = -2 وبالتالي فإن المصفوفة العمودية التي تمثل الإزاحة هي: (-5, -2)T = e يؤشر شعاع الإزاحة إلى الاتجاه المعاكس عند زيارة النقاط بالترتيب المعاكس، ويكون كل عنصر ناتجًا عن ضرب القيمة القديمة بالعدد -1 في المصفوفة العمودية. تختلف الإزاحة من النقطة A إلى النقطة B عن الإزاحة من النقطة B إلى النقطة A، إذ يمكن التفكير في الإزاحة بوصفها "إرشادات حول كيفية المشي من نقطة إلى أخرى"؛ لذا إذا كنت واقفًا عند النقطة A وترغب في الوصول إلى النقطة B، فإن الإزاحة (5, 2)T تقول "امشِ 5 وحدات في الاتجاه الموجب X، ثم امشِ وحدتين في الاتجاه الموجب Y". تحتاج بالطبع إلى اتجاهات مختلفة للانتقال من النقطة B إلى النقطة A، حيث تقول الإزاحة (-5, -2)T "امشِ 5 وحدات في الاتجاه السالب X، ثم امشِ وحدتين في الاتجاه السالب Y"، مما يعيدك إلى النقطة A. يمكن التعبير عن الإزاحة من نقطة البداية إلى نقطة النهاية كما يلي: الإزاحة = (x نقطة النهاية - x نقطة البداية, y نقطة النهاية - y نقطة البداية)T لنفترض أن النقطة C هي: x=4, y=2 وأن النقطة D هي: x=3, y= 5، فما هي المصفوفة العمودية التي تمثل الإزاحة من C إلى D؟ X لنقطة النهاية - X لنقطة البداية = 3 - 4 = -1 Y لنقطة النهاية - Y لنقطة البداية = 5 - 2 = 3 إذًا فالمصفوفة العمودية هي: (-1, 3)T قراءة الإزاحات من الرسوم البيانية الورقية يوضح الرسم البياني التالي شعاعًا بين نقطتين، حيث يمكن قراءة عناصر الإزاحة بين النقطتين من الرسم البياني من خلال حساب عدد المربعات من ذيل الشعاع إلى رأسه: (-3, 5)T. افعل الشيء ذاته الآن مع الرسم البياني التالي: ما هي الإزاحة من النقطة E إلى النقطة F؟ الإزاحة هي: (7, 3)T للتحقق من إجابتك، ابدأ من نقطة البداية واتبع التعليمات التالية: امشِ بمقدار 7 وحدات على محور X، ثم امشِ بمقدار 3 وحدات على محور Y. إذا كانت إجابتك صحيحة، فسينتهي بك الأمر عند نقطة النهاية. ليس للأشعة موقع محدد ما هي الإزاحة من النقطة G: (-3, 4)T إلى النقطة H: (5, -2)T؟ يمكن الإجابة على هذا السؤال من خلال طرح قيم نقطة البداية G من القيم المقابلة لنقطة النهاية H ويعطي ذلك: (8, -6)T. احسب الآن الإزاحة من النقطة M إلى النقطة N في الرسم البياني التالي من خلال طرح قيم نقطة البداية N من القيم المقابلة لنقطة النهاية M، ويعطي ذلك: (8, -6)T، وهي القيمة نفسها للرسم البياني السابق. يُعَد شعاع الإزاحة هندسيًا من النقطة G إلى النقطة H هو شعاع الإزاحة نفسه من النقطة M إلى النقطة N، وتكون المصفوفتان العموديتان متساويتين باستخدام قاعدة مساواة المصفوفات العمودية. يُعَد ذلك منطقيًا، لأنك تقطع المسافة والاتجاه نفسه للمشي من النقطة M إلى النقطة N عند المشي من النقطة G إلى النقطة H، وبالتالي توضح هذه الرسوم البيانية الإزاحات مع الطول والاتجاه نفسه ولكن مع نقاط بداية مختلفة، فليس للأشعة موقعٌ محدد. من الشائع رسم الشعاع على شكل سهم في الرسوم البيانية، ذيله عند نقطة ورأسه عند نقطة أخرى، ولكن يمكن أن يمثل أيّ سهم له الطول والاتجاه نفسه هذا الشعاع. طرح النقاط هل تُعَدّ الإزاحة بين نقطتين فريدة؟ نعم، هذا صحيح. يمكن كتابة صيغة حساب شعاع الإزاحة على النحو التالي، إذ تساوي الإزاحة من النقطة S (نقطة البداية) إلى النقطة F (نقطة النهاية) ما يلي: F - S = ( Xf , Yf )T - ( Xs , Ys )T = ( Xf-Xs , Yf-Ys )T تُستخدَم نقطتان في هذه العملية لإنتاج شعاع واحد، فمن الغريب أن ينتج عن طرح كائنين لهما النوع نفسه كائنٌ من نوع آخر، وهذا يقودنا إلى السؤال التالي: إذا طرحنا نقطةً ثلاثية الأبعاد من نقطة أخرى ثلاثية الأبعاد، فهل ستكون النتيجة شعاعًا؟ نعم، إذ تعمل هذه الطريقة في الفضاء ثلاثي الأبعاد وثنائي الأبعاد. تدريب عملي لحساب الإزاحة لنفترض أن النملة ريم ضاعت عند النقطة (8, 4)T، فأوجد الإزاحة التي ستوصِلها إلى صديقتها سلمى عند النقطة (2, 2)T، أو ما هي الإزاحة التي يجب أن تتحرك بها ريم للعثور على صديقتها سلمى؟ لنطرح النقاط (سلمى - ريم): (2, 2)T - (8, 4)T = (-6, -2)T ستحصل على الإجابة نفسها من خلال عدّ مربعات ورقة الرسم البياني. استخدام الأعداد الحقيقية قد نرغب في تجنب الأعداد السالبة وبالتالي سنحسب الإزاحة بصورة غير صحيحة، إذ يُعَد وجود إزاحات سالبة أمرًا طبيعيًا، فالجزء "السلبي" مطلوب لإظهار الاتجاه. شيء آخر يجب أخذه في الحسبان وهو أن عناصر المصفوفة العمودية هي أعداد حقيقية، ولكن استخدمنا في الأمثلة أعدادًا صحيحة لتسهيل الأمور فقط، فلا يوجد أيّ خطأ في المصفوفة العمودية (-1.2304, 9.3382)T. لنفترض أن لدينا النقطتان: النقطة B وهي: ( 4.75, 6.23 ) والنقطة A وهي: ( 1.25, 4.03 )، فما هي الإزاحة من النقطة A إلى النقطة B؟ الإزاحة من النقطة A إلى النقطة B هي: ( 4.75, 6.23 ) - ( 1.25, 4.03 ) = ( 3.50, 2.20 )T وصلنا إلى نهاية هذا المقال، وسنناقش في المقال التالي العمليات على الأشعة والعمليات المكافئة باستخدام المصفوفات العمودية. ترجمة -وبتصرُّف- للفصل Vectors, Points, and Column Matrices من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ المزيد المقال السابق التعرف على النقاط والخطوط في الرسوميات الحاسوبية ثلاثية الأبعاد. الرسم باستخدام الأدوات المساعدة في كريتا دليلك الشامل إلى أنواع البيانات.
-
أنشأتَ واختبرتَ موقع المكتبة المحلية LocalLibrary، ويجب الآن تثبيته على خادم ويب عام ليصل إليه موظفو المكتبة وأعضاؤها عبر الإنترنت. يقدّم هذا المقال نظرة عامة حول كيفية البحث عن مضيف لنشر موقعك، وما عليك فعله لتجهيز موقعك لمرحلة الإنتاج. المتطلبات الأساسية: إكمال المقالات السابقة من هذه السلسلة بما في ذلك مقال عرض بيانات المكتبة والعمل مع الاستمارات. الهدف: معرفة مكان وكيفية نشر تطبيق Express في بيئة الإنتاج. يجب استضافة موقعك بعد الانتهاء منه (أو الانتهاء منه "بما يكفي" لبدء الاختبار العام) في مكان أعم ويمكن الوصول إليه من أجهزة أخرى غير حاسوبك الشخصي الخاص بالتطوير. عملتَ حتى الآن في بيئة تطوير باستخدام خادم ويب تطوير Express/Node لمشاركة موقعك على المتصفح أو الشبكة المحلية، وشغّلتَ موقعك باستخدام إعدادات تطوير (غير آمنة) بحيث يمكن الوصول إلى معلومات تنقيح الأخطاء والمعلومات الخاصة الأخرى. يجب عليك أولًا تنفيذ الخطوات التالية قبل أن تتمكّن من استضافة موقع ويب خارجيًا: اختيار بيئة لاستضافة تطبيق Express. إجراء بعض التغييرات على إعدادات مشروعك. إعداد بنية تحتية على مستوى الإنتاج لتخديم موقع الويب. يقدّم هذا المقال بعض الإرشادات حول الخيارات المتاحة لاختيار موقع استضافة، ونظرة ًعامة مختصرة على ما يجب تطبيقه لتجهيز تطبيق Express لبيئة الإنتاج، ويقدم مثالًا عمليًا لكيفية تثبيت موقع المكتبة المحلية LocalLibrary على خدمة الاستضافة السحابية Railway. ما هي بيئة الإنتاج؟ بيئة الإنتاج هي البيئة التي يوفّرها حاسوب الخادم إذ ستشغِّل موقعك للاستخدام الخارجي، وتشمل هذه البيئة ما يلي: عتاد الحاسوب الذي يعمل عليه الموقع. نظام التشغيل، مثل لينكس أو ويندوز. وقت التشغيل الخاص بلغة البرمجة ومكتبات أطر العمل التي كُتِب عليها موقعك. خادم الويب المُستخدَم لتخديم الصفحات والمحتويات الأخرى، مثل خادم إنجن إكس Nginx وأباتشي Apache. البنية التحتية لخادم الويب وتتضمن خادم الويب والوكيل العكسي Reverse Proxy وموازن الحِمل Load Balancer وغير ذلك. قواعد البيانات التي يعتمد عليها موقعك. يمكن أن يكون حاسوب الخادم موجودًا في مقر عملك ومتصلًا بالإنترنت من خلال رابط سريع، ولكن يمكن استخدام حاسوب مستضاف على السحابة، وهذا يعني تنفيذ شيفرتك البرمجية على بعض الحواسيب البعيدة (أو على حاسوب افتراضي) في مركز أو مراكز بيانات شركتك المضيفة. يقدّم الخادم البعيد عادةً مستوًى مضمونًا من موارد المعالجة، مثل وحدة المعالجة المركزية والذاكرة RAM وذاكرة التخزين وغير ذلك والاتصال بالإنترنت بسعر معين. يشار إلى هذا النوع من عتاد المعالجة والتشبيك الذي يمكن الوصول إليه عن بُعد باسم البنية التحتية كخدمة Infrastructure as a Service -أو IaaS اختصارًا. يوفّر العديد من بائعي خدمة IaaS خيارات التثبيت المُسبَق لنظام تشغيل معين، والذي يجب تثبيت مكونات بيئتك الإنتاجية الأخرى عليه، ويسمح لك البائعون الآخرون باختيار بيئات كاملة الميزات، ويمكن أن يتضمن ذلك إعداد بيئة Node الكاملة. ملاحظة: يمكن للبيئات المبنية مسبقًا أن تسهّل إعداد موقعك لأنها تقلل من عملية الضبط Configuration، ولكن يمكن أن تقيّدك الخيارات المتاحة بخادم غير مألوف لك (أو بمكونات أخرى) ويمكن أن تستند إلى نسخة أقدم من نظام التشغيل. يُفضَّل غالبًا تثبيت المكونات بنفسك حتى تحصل على المكونات التي تريدها، وبالتالي ستعرف من أين تبدأ عندما تحتاج إلى ترقية أجزاء من النظام. يدعم مزوّدو الاستضافة الآخرون إطار عمل Express بوصفه جزءًا من استضافة المنصة كخدمة Platform as a Service -أو PaaS اختصارًا، فلا داعي للقلق في هذا النوع من الاستضافة بشأن معظم أجزاء بيئتك الإنتاجية (الخوادم وموازنو الحِمل load balancers وغيرها) لأن المنصة المضيفة تهتم بهذه الأشياء نيابةً عنك، مما يجعل النشر سهلًا جدًا، لأنك تحتاج فقط إلى التركيز على تطبيق الويب وليس على بنية الخادم التحتية الأخرى. سيختار بعض المطورين المرونة المتزايدة التي توفرها استضافة IaaS على حساب استضافة PaaS، بينما سيقدّر المطورون الآخرون تقليل تكاليف الصيانة وسهولة توسيع نطاق استضافة PaaS. يكون إعداد موقعك على نظام PaaS أسهل بكثير في البداية، وهذا ما سنفعله في هذا المقال. ملاحظة: إذا اخترت مزوّد استضافة متوافق مع Node/Express، فيجب أن يقدّم إرشادات حول كيفية إعداد موقع Express باستخدام عمليات ضبط مختلفة لخادم الويب وخادم التطبيق والوكيل العكسي وغير ذلك. اختيار مزود الاستضافة يوجد العديد من مزوّدات الاستضافة المعروفة التي تدعم بنشاط أو تعمل بصورة جيدة مع بيئة Node و Express، ويوفّر هؤلاء البائعون أنواعًا مختلفة من البيئات (IaaS و PaaS) ومستويات مختلفة من موارد المعالجة والشبكات بأسعار مختلفة. ملاحظة: هناك الكثير من حلول الاستضافة، ويمكن أن تتغير خدماتها وأسعارها بمرور الوقت. سنقدّم بعض الخيارات، ولكن يجدر بك التحقق من هذه الخيارات وغيرها قبل اختيار مزود الاستضافة. إليك بعض الأشياء التي يجب مراعاتها عند اختيار المضيف: مدى انشغال موقعك المُحتمَل وتكلفة البيانات وموارد المعالجة المطلوبة لتلبية هذا المطلب. مستوى الدعم للتوسّع أفقيًا (إضافة المزيد من الأجهزة) وعموديًا (الترقية إلى أجهزة أقوى) وتكاليف ذلك. مكان مراكز بيانات المزوّد، أي المكان الذي يكون الوصول إليه أسرع. أداء وقت التشغيل ووقت التعطل السابقَين للمضيف. الأدوات المتوفرة لإدارة الموقع، لمعرفة إذا كانت سهلة الاستخدام وآمنة، مثل استخدام بروتوكول SFTP أو استخدام بروتوكول FTP. أطر العمل المبنية مسبقًا لمراقبة خادمك. القيود المعروفة، إذ سيوقِف بعض المضيفين عمدًا خدمات معينة مثل البريد الإلكتروني، ويقدّم البعض الآخر فقط عددًا معينًا من ساعات "النشاط" في بعض مستويات الأسعار، أو يقدّم فقط قدرًا صغيرًا من التخزين. الفوائد الإضافية، إذ ستقدّم بعض المزوّدات أسماء نطاقات مجانية ودعمًا لشهادات SSL التي يمكن أن يتعيّن عليك دفع ثمنها. معرفة ما إذا كان المستوى "المجاني" الذي تعتمد عليه تنتهي صلاحيته بمرور الوقت، وما إذا كانت تكلفة التهجير Migrating إلى مستوًى أغلى تعني أنه كان من الأفضل استخدام بعض الخدمات الأخرى من البداية. هناك عددٌ قليل جدًا من المواقع التي توفر بيئات معالجة "مجانية" مخصصة للتقييم والاختبار في البداية، وتكون عادةً بيئات مُقيَّدة أو محدودة الموارد إلى حدٍ ما، ويجب أن تدرك أنه يمكن أن تنتهي صلاحيتها بعد فترة أولية أو يكون لديها قيود أخرى، ولكنها رائعة لاختبار المواقع ذات حركة المرور المنخفضة في بيئة مُستضافة، ويمكن أن توفر تهجيرًا سهلًا للدفع مقابل المزيد من الموارد عندما يصبح موقعك أكثر انشغالًا. تشمل الخيارات الشائعة في هذه الفئة Railway و Python Anywhere و Amazon Web Services و Microsoft Azure وغير ذلك. يقدّم معظم المزوّدين مستوًى أساسيًا مخصصًا لمواقع الإنتاج الصغيرة، والذي يوفّر مستويات أكثر فائدة من قدرة المعالجة وقيودًا أقل. يُعَد Heroku و Digital Ocean و Python Anywhere أمثلة على مزودي الاستضافة المشهورة التي لديها مستوى معالجة أساسي غير مكلف نسبيًا (في نطاق يتراوح بين 5 و 10 دولارات أمريكية شهريًا). ملاحظة: تذكّر أن السعر ليس معيار الاختيار الوحيد، فإذا كان موقعك ناجحًا، فيمكن أن تكون قابلية التوسع هي الأهم. تجهيز موقعك للنشر الأشياء الرئيسية التي يجب التفكير فيها عند نشر موقعك هي أمان الويب والأداء، إذ ستحتاج في الحد الأدنى إلى إزالة تعقّب المكدس المُضمَّن في صفحات الخطأ أثناء التطوير، وترتيب التسجيل، وضبط الترويسات المناسبة لتجنب العديد من هجمات الأمان الشائعة، لذا سنحدّد في الأقسام الفرعية التالية أهم التغييرات الواجب إجراؤها على تطبيقك. ملاحظة: هناك نصائح مفيدة أخرى في توثيق Express، لذا اطلع على أفضل ممارسات عملية الإنتاج: الأداء والموثوقية والأمان، واعتبارات نشر مشاريع Node.js وExpress على الويب. ضبط قاعدة البيانات استخدمنا حتى الآن قاعدة بيانات واحدة ثابتة في الملف app.js، وبسبب عدم احتواء قاعدة بيانات التطوير على معلومات نخشى من كشفها أو إتلافها، فليس هناك خطورة في حال تسريبها، ولكن في حال كنا نتعامل مع بيانات حقيقية، مثل معلومات المستخدم الشخصية، ستكون حماية تلك البيانات أمرًا مهمًا جدًا. نريد عادةً أن نكون قادرين على امتلاك قاعدة بيانات مختلفة لكل من الإنتاج والتطوير وكذلك الاحتفاظ بقاعدة بيانات الإنتاج منفصلة أيضًا عن الشيفرة المصدرية بهدف حمايتها. إذا كان مزود الاستضافة الخاص بك داعمًا لمتغيرات البيئة من خلال واجهة الويب، ستكون إحدى الطرق لتحقيق ما سبق هي جعل الخادم يحصل على عنوان URL لقاعدة البيانات من متغير البيئة، لذلك سنعدّل موقع المكتبة المحلية LocalLibrary للحصول على معرّف URI لقاعدة البيانات من بيئة نظام التشغيل (إذا كان معرّفًا)، وإلّا فسنستخدم قاعدة بيانات التطوير الخاصة بنا. افتح الملف app.js وابحث عن السطر الذي يضبط متغير اتصال قاعدة بيانات MongoDB، والذي سيبدو كما يلي: const mongoDB = "mongodb+srv://your_user_name:your_password@cluster0.lz91hw2.mongodb.net/local_library?retryWrites=true&w=majority"; ضع الشيفرة التالية مكان السطر السابق، إذ تستخدم هذه الشيفرة process.env.MONGODB_URI للحصول على سلسلة الاتصال النصية من متغير البيئة MONGODB_URI إذا جرى ضبطه، واستخدم عنوان URL لقاعدة بياناتك بدلًا من العنصر البديل فيما يلي: // إعداد اتصال mongoose const dev_db_url = const mongoose = require("mongoose"); mongoose.set("strictQuery", false); const dev_db_url = "mongodb+srv://your_user_name:your_password@cluster0.lz91hw2.mongodb.net/local_library?retryWrites=true&w=majority"; const mongoDB = process.env.MONGODB_URI || dev_db_url; main().catch((err) => console.log(err)); async function main() { await mongoose.connect(mongoDB); } ضبط متغير البيئة NODE_ENV على القيمة production يمكننا إزالة تعقّب المكدس في صفحات الخطأ من خلال ضبط متغير البيئة NODE_ENV على قيمة الإنتاج 'production'، إذ يُضبَط على قيمة التطوير 'development' افتراضيًا. يؤدي ضبط المتغير على قيمة الإنتاج إلى إنشاء رسائل خطأ أقل تفصيلًا، بالإضافة إلى التخزين المؤقت لقوالب العرض وملفات CSS المُولَّدة من توسّعات شيفرة CSS، وتشير الاختبارات إلى أن ضبط متغير البيئة NODE_ENV على الإنتاج يمكن أن يحسن أداء التطبيق بمقدار ثلاثة أضعاف. يمكن إجراء هذا التغيير إما باستخدام export، أو ملف بيئة، أو نظام تهيئة لنظام التشغيل. ملاحظة: يُعَد ذلك تغييرًا تجريه في إعداد بيئتك بدلًا من إجرائه في تطبيقك، ولكنه جدير بالذكر حاليًا، إذ سنوضح كيفية ضبطه لمثال الاستضافة الآتي. التسجيل المناسب يمكن أن يكون لتسجيل الاستدعاءات تأثير على موقع الويب الذي يمتلك حركة مرور عالية، إذ يمكن أن تحتاج إلى تسجيل نشاط موقع الويب في بيئة الإنتاج، مثل تعقّب حركة المرور، أو تسجيل استدعاءات واجهة برمجة التطبيقات، ولكن يجب أن تحاول تقليل كمية التسجيل المُضافة لأغراض تنقيح الأخطاء Debugging. تتمثل إحدى طرق تقليل تسجيل "تنقيح الأخطاء" في بيئة الإنتاج في استخدام وحدة مثل الوحدة debug التي تسمح لك بالتحكم في التسجيل الذي يُجرَى من خلال ضبط متغير بيئة، فمثلًا يوضح جزء الشيفرة البرمجية التالي كيف يمكنك إعداد تسجيل المؤلف "author"، إذ يُصرَّح عن المتغير debug بالاسم author، وستُعرَض البادئة "author" تلقائيًا لجميع السجلات من هذا الكائن. const debug = require("debug")("author"); // عرض استمارة تحديث المؤلف باستخدام GET exports.author_update_get = asyncHandler(async (req, res, next) => { const author = await Author.findById(req.params.id).exec(); if (author === null) { // لا توجد نتائج debug(`id not found on update: ${req.params.id}`); const err = new Error("Author not found"); err.status = 404; return next(err); } res.render("author_form", { title: "Update Author", author: author }); }); يمكنك بعد ذلك تفعيل مجموعة معينة من السجلات من خلال تحديدها بوصفها قائمة مفصول بين عناصرها بفواصل في متغير بيئة DEBUG، ويمكنك ضبط المتغيرات لعرض سجلات المؤلف والكتاب كما هو موضح فيما يلي (المحارف البديلة Wildcards مدعومة أيضًا): # في نظام ويندوز set DEBUG=author,book # في نظام لينكس export DEBUG="author,book" ملاحظة: يمكن أن تحل استدعاءات debug محل التسجيل الذي ربما طبّقته مسبقًا باستخدام console.log() أو console.error()، لذا ضع التسجيل باستخدام وحدة debug مكان استدعاءات console.log() في شيفرتك البرمجية، وشغّل التسجيل وأوقفه في بيئة التطوير من خلال ضبط المتغير DEBUG ولاحظ تأثير ذلك على التسجيل. إذا كنت بحاجة إلى تسجيل نشاط موقع الويب، يمكنك استخدام مكتبة تسجيل مثل Winston أو Bunyan. اطلع على أفضل ممارسات عملية الإنتاج: الأداء والموثوقية لمزيد من المعلومات. استخدم ضغط gzip أو deflate للاستجابة تضغط خوادم الويب غالبًا استجابة HTTP المُرسَلة إلى العميل، مما يقلل من الوقت المطلوب للعميل للحصول على الصفحة وتحميلها. سيعتمد تابع الضغط المُستخدَم على توابع فك الضغط التي يدعمها العميل في الطلب، إذ ستُرسَل الاستجابة بحيث تكون غير مضغوطة عندما تكون توابع الضغط غير مدعومة. يمكنك إضافة ذلك إلى موقعك باستخدام برمجية الضغط الوسيطة compression، لذا ثبّتها في جذر مشروعك من خلال تشغيل الأمر التالي: npm install compression افتح الملف "./app.js" واطلب مكتبة الضغط. ضِف مكتبة الضغط إلى سلسلة البرمجيات الوسيطة باستخدام التابع use() الذي يجب أن يظهر قبل أي وجهات Routes تريد ضغطها (جميع الوجهات في حالتنا): const catalogRouter = require("./routes/catalog"); // استيراد الوجهات لمنطقة الدليل "catalog" من موقعك const compression = require("compression"); // إنشاء كائن تطبيق Express const app = express(); // … app.use(compression()); // ضغط جميع الوجهات app.use(express.static(path.join(__dirname, "public"))); app.use("/", indexRouter); app.use("/users", usersRouter); app.use("/catalog", catalogRouter); // إضافة وجهات الدليل catalog إلى سلسلة البرمجيات الوسيطة // … ملاحظة: لن تَستخدمَ هذه البرمجيات الوسيطة لموقع الويب الذي يمتلك حركة مرور عالية في بيئة الإنتاج، لذا يمكنك استخدام وكيل عكسي مثل Nginx بدلًا من ذلك. استخدام حزمة Helmet للحماية من الثغرات المعروفة تُعَد Helmet حزمة برمجيات وسيطة يمكنها ضبط ترويسات HTTP المناسبة التي تساعد في حماية تطبيقك من ثغرات الويب المعروفة. اطّلع على توثيقها للحصول على مزيد من المعلومات حول الترويسات التي تضبطها والثغرات الأمنية التي تحميك منها. ثبّتها في جذر مشروعك من خلال تشغيل الأمر التالي: npm install helmet افتح الملف "./app.js" واطلب مكتبة helmet، ثم ضِف الوحدة إلى سلسلة البرمجيات الوسيطة باستخدام التابع use(): const compression = require("compression"); const helmet = require("helmet"); // إنشاء كائن تطبيق Express const app = express(); // إضافة حزمة helmet إلى سلسلة البرمجيات الوسيطة // ضبط ترويسات CSP للسماح بتخديم Bootstrap و Jquery app.use( helmet.contentSecurityPolicy({ directives: { "script-src": ["'self'", "code.jquery.com", "cdn.jsdelivr.net"], }, }), ); // … يمكن أن نضيف التابع app.use(helmet()); لإضافة مجموعة فرعية من الترويسات المتعلقة بالأمان والتي تكون منطقية لمعظم المواقع، ولكننا نضمّن بعض سكربتات بوتستراب bootstrap و jQuery في قالب موقع المكتبة المحلية LocalLibrary الأساسي، مما يؤدي إلى انتهاك سياسة أمان المحتوى Content Security Policy -أو CSP اختصارًا- الافتراضية الخاصة بحزمة helmet، والتي لا تسمح بتحميل السكربتات العابرة للمواقع. يمكن السماح بتحميل هذه السكربتات من خلال تعديل ضبط helmet بحيث تُضبَط موجّهات CSP للسماح بتحميل السكربت من النطاقات المُشار إليها، ويمكنك بالنسبة لخادمك إضافة أو تعطيل ترويسات محددة حسب الحاجة باتباع الإرشادات الخاصة باستخدام helmet. إضافة معدل محدود إلى وجهات واجهة برمجة التطبيقات API تُعَد Express-rate-limit حزمة برمجية وسيطة يمكن استخدامها للحد من الطلبات المتكررة لواجهات برمجة التطبيقات API والنقاط النهائية، فهناك العديد من الأسباب التي يمكن أن تؤدي إلى زيادة الطلبات على موقعك مثل هجمات حجب الخدمة أو هجمات القوة الغاشمة أو حتى مجرد عميل أو سكربت لا يتصرف كما هو متوقع، ويمكن أن تُحاسَب على حركة المرور الإضافية بغض النظر عن مشاكل الأداء التي يمكن أن تنشأ عن كثرة الطلبات التي تتسبب في إبطاء خادمك. يمكن استخدام حزمة Express-rate-limit للحد من عدد الطلبات التي يمكن إجراؤها على وجهة معينة أو مجموعة من الوِجهات. يمكنك الاطلاع على مقال الهجمات الأمنية Security Attacks في الشبكات الحاسوبية على أكاديمية حسوب لمزيد من المعلومات حول أنواع الهجمات الأمنية. ثبّت هذه الحزمة في جذر مشروعك من خلال تشغيل الأمر التالي: npm install express-rate-limit افتح الملف "./app.js" واطلب مكتبة express-rate-limit كما يلي، ثم ضِف الوحدة إلى سلسلة البرمجيات الوسيطة باستخدام التابع use(): const compression = require("compression"); const helmet = require("helmet"); const app = express(); // إعداد محدِّد المعدل إلى عشرين طلبًا في الدقيقة كحد أعلى const RateLimit = require("express-rate-limit"); const limiter = RateLimit({ windowMs: 1 * 60 * 1000, // 1 دقيقة max: 20, }); // طبّق محدّد المعدّل على جميع الطلبات app.use(limiter); // … ملاحظة: يحدّد الأمر السابق جميع الطلبات لتكون 20 طلبًا في الدقيقة، ولكن يمكنك تغييره حسب الحاجة. يمكن أيضًا استخدام خدمات خارجية مثل Cloudflare إذا كنت بحاجة إلى مزيد من الحماية المتقدمة ضد هجمات حجب الخدمة أو أنواع أخرى من الهجمات. تطبيق عملي: تثبيت موقع المكتبة المحلية LocalLibrary على منصة Railway يقدّم هذا القسم شرحًا عمليًا لكيفية تثبيت موقع المكتبة المحلية LocalLibrary على منصة Railway. سبب استخدام Railway اخترنا استخدام منصة Railway لعدة أسباب هي: تمتلك Railway مستوًى مجانيًا لخطة بداية مع بعض القيود، إذ من المهم أن تكون بأسعار مقبولة لجميع المطورين. تهتم Railway بمعظم البنية التحتية، فلا حاجة للقلق بشأن الخوادم وموازني الحِمل والوكلاء العكسيين وغير ذلك، مما يجعل البدء أسهل بكثير. تركّز Railway على تجربة المطور للتطوير والنشر، مما يؤدي إلى وجود منحنى تعليمي أسرع وأسلس من العديد من البدائل الأخرى. تُعَد المهارات والمفاهيم التي ستتعلمها عند استخدام Railway قابلة للتحويل، إذ تمتلك Railway بعض الميزات الجديدة الممتازة، ولكن تستخدم خدماتُ الاستضافة الشائعة الأخرى العديدَ من الأفكار والأساليب نفسها. لا تؤثر قيود الخدمات والخطط على استخدامنا لمنصة Railway في مثالنا، فمثلًا: تقدّم خطة البداية 500 ساعة فقط من وقت النشر المستمر كل شهر و5 دولارات من الرصيد الذي يُستهلَك بناءً على الاستخدام، ويُعاد ضبط الساعات والرصيد ويجب إعادة نشر المشاريع في نهاية كل شهر. تعني هذه القيود أنه يمكنك تشغيل هذا المثال بصورة مستمرة لمدة 21 يومًا تقريبًا، ويُعَد ذلك أكثر من كافٍ للتطوير والاختبار، ولكن لن تتمكّن من استخدام هذه الخطة لموقع حقيقي للإنتاج. تحتوي بيئة خطة البداية على 512 ميجابايت فقط من الذاكرة RAM و1 جيجابايت من ذاكرة التخزين، وهذا أكثر من كافٍ لمثالنا. لا توجد سوى منطقة واحدة مدعومة وهي الولايات المتحدة الأمريكية حاليًا، إذ يمكن أن تكون الخدمة خارج هذه المنطقة أبطأ أو تحظرها القوانين المحلية. يمكن العثور على قيود أخرى في توثيق خطط Railway للدفع. تبدو الخدمة موثوقة جدًا، وإذا كانت مناسبة لك، فإن الأسعار يمكن التنبؤ بها، ويكون توسيع تطبيقك سهلًا جدًا. تعَد Railway مناسبة لاستضافة مثالنا، ولكن يجب أن تأخذ الوقت الكافي لتحديد ما إذا كانت مناسبة لموقعك. كيفية عمل Railway يُشغَّل كل تطبيق ويب في حاوية افتراضية معزولة ومستقلة خاصة به، لذا يجب أن تكون Railway قادرة على إعداد البيئة والاعتماديات المناسبة، وفهم كيفية إطلاقها لتنفيذ تطبيقك. تجعل Railway هذا الأمر سهلًا، إذ يمكنها التعرف تلقائيًا على العديد من أطر عمل وبيئات تطبيقات الويب المختلفة وتثبيتها بناءً على استخدامها "للمصطلحات الشائعة"، فمثلًا تتعرّف Railway على تطبيقات Node لأن لديها ملف package.json، ويمكنها تحديد مدير الحزم المُستخدَم للبناء من ملف "القفل Lock". إذا احتوى التطبيق على ملف package-lock.json مثلًا، فستعرف Railway أنها ستستخدم مدير حزم npm لتثبيت الحزم، وإذا وجدت yarn.lock فستعرف أنها ستستخدم yarn. ستبحث Railway -بعد تثبيت جميع الاعتماديات- عن سكربتات بالاسم "build" و "start" في ملف الحزمة، وستستخدمها لبناء وتشغيل الشيفرة البرمجية. ملاحظة: تستخدم Railway حزمة Nixpacks للتعرف على العديد من أطر عمل تطبيقات الويب المكتوبة بلغات برمجة مختلفة. لا حاجة إلى معرفة أيّ شيء آخر لهذا المقال، ولكن يمكنك معرفة المزيد حول خيارات نشر تطبيقات Node في توثيق Nixpacks. يمكن للتطبيق بعد تشغيله ضبط نفسه باستخدام المعلومات المقدمة في متغيرات البيئة، فمثلًا يجب أن يحصل التطبيق الذي يستخدم قاعدة بيانات على العنوان باستخدام متغير، ويمكن أن تستضيف Railway خدمة قاعدة البيانات نفسها أو على أي مزوّد آخر. يتفاعل المطورون مع Railway من خلال موقع Railway وباستخدام أداة واجهة سطر أوامر CLI خاصة، إذ تسمح لك واجهة CLI بربط مستودع غيت هب GitHub محلي بمشروع Railway، ورفع المستودع من الفرع المحلي إلى الموقع المباشر، وفحص سجلات العملية الجارية، وإعداد متغيرات الضبط والحصول عليها وغير ذلك. من أهم الميزات أنه يمكنك استخدام واجهة CLI لتشغيل مشروعك المحلي مع متغيرات البيئة نفسها للمشروع المباشر. يجب وضع تطبيق Express الخاص بنا في مستودع غيت git وإجراء بعض التغييرات الطفيفة ليعمل تطبيقنا على Railway، ثم يمكننا إعداد حساب على Railway وتثبيت موقعنا وقاعدة البيانات وتجربة عميل Railway. وهذا هو كل ما تحتاجه من معلومات في البداية. إنشاء مستودع للتطبيق على غيت هب GitHub تتكامل Railway تكاملًا وثيقًا مع غيت هب ونظام التحكم في إصدارات الشيفرة المصدرية git، ويمكنك ضبطها لنشر التحديثات تلقائيًا عند إجراء تغييرات إلى مستودع أو فرع معين على غيت هب، أو يمكنك دفع فرع الشيفرة المحلية الحالي مباشرةً إلى النشر الخاص بمنصة Railway باستخدام واجهة CLI. ملاحظة: يُعَد استخدام نظام إدارة الشيفرة المصدرية مثل غيت هب ممارسة جيدة لتطوير البرمجيات، فإذا كنت تستخدم غيت هب لإدارة شيفرتك المصدرية مسبقًا، فتخطى هذه الخطوة. هناك العديد من الطرق للعمل مع git، ومن أسهلها إعداد حساب على غيت هب أولًا وإنشاء المستودع عليه، ثم المزامنة معه محليًا كما يلي: انتقل إلى موقع GitHub الرسمي وأنشئ حسابًا عليه. انقر على ارتباط + في شريط الأدوات العلوي وحدّد خيار "مستودع جديد New repository" بعد تسجيل الدخول. املأ جميع الحقول في هذه الاستمارة، إذ يمكن أن تكون هذه الحقول غير إلزامية، ولكن يُوصَى بها بشدة. أدخل اسم المستودع الجديد والوصف، فمثلًا يمكنك استخدام الاسم "express-locallibrary-tutorial" والوصف "Local Library website written in Express (Node)" (موقع المكتبة المحلية المكتوب باستخدام Express). اختر الخيار Node في قائمة الاختيار Add .gitignore. اختر الترخيص المفضل لديك في قائمة الاختيار Add license. تحقق من تهيئة المستودع باستخدام README. تحذير: سيجعل الوصول الافتراضي العام "Public" جميع الشيفرة البرمجية المصدرية -بما في ذلك اسم المستخدم وكلمة المرور لقاعدة البيانات- مرئيةً لأي شخص على الإنترنت، لذا تأكد من أن الشيفرة البرمجية المصدرية لا تقرأ اعتماديات إلّا من متغيرات البيئة ولا تحتوي على أيّ اعتماديات ثابتة، وإلّا حدد الخيار خاص "Private" للسماح فقط لأشخاص محدَّدين برؤية الشيفرة المصدرية. اضغط على إنشاء مستودع Create repository. انقر فوق الزر الأخضر نسخ Clone أو تنزيل Download في صفحة مستودعك الجديد. انسخ قيمة URL من حقل النص الموجود في مربع الحوار الذي يظهر، فإذا استخدمت اسم المستودع "express-locallibrary-tutorial"، فيجب أن يكون عنوان URL مثل العنوان: https://github.com/<your_git_user_id>/express-locallibrary-tutorial.git. انتهينا من إنشاء المستودع، ويجب الآن نسخه على حاسوبك المحلي باتباع الخطوات التالية: أولًا، ثبّت git على حاسوبك المحلي، إذ يمكنك العثور على نسخ لأنظمة مختلفة. ثانيًا، افتح موجه الأوامر أو الطرفية وانسخ مستودعك باستخدام عنوان URL الذي نسخته سابقًا كما في الأمر التالي: git clone https://github.com/<your_git_user_id>/express-locallibrary-tutorial.git سيؤدي هذا الأمر إلى إنشاء المستودع في المجلد الحالي. ثالثًا، انتقل إلى مجلد المستودع باستخدام الأمر التالي: cd express-locallibrary-tutorial أخيرًا، انسخ ملفات تطبيقك المصدرية في مجلد المستودع، ثم اجعلها جزءًا من المستودع باستخدام git كما يلي: أولًا، انسخ تطبيق Express إلى هذا المجلد باستثناء المجلد "/node_modules" الذي يحتوي على ملفات الاعتماديات التي يجب جلبها من مدير حزم npm حسب الحاجة. ثانيًا، افتح موجه الأوامر أو الطرفية واستخدم الأمر add لإضافة جميع الملفات إلى git كما يلي: git add -A ثالثًا، استخدم الأمر status للتحقق من صحة جميع الملفات التي تريد تثبيتها commit، إذ نريد تضمين الملفات المصدرية، وليس الملفات الثنائية والمؤقتة وما إلى ذلك، إذ يجب أن تبدو كما يلي: > git status ويكون الخرج الناتج بعد تنفيذ هذا الأمر على النحو التالي: On branch main Your branch is up-to-date with 'origin/main'. Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: … رابعًا، ثبّت باستخدام commit الملفات في مستودعك المحلي عندما تكون راضيًا عن النتيجة، إذ يكافئ ذلك الإقرار بالتغييرات وجعلها جزءًا رسميًا من المستودع المحلي. git commit -m "First version of application moved into GitHub" خامسًا، لم يتغيّر المستودع البعيد حتى الآن، لذا يجب مزامنة (باستخدام الأمر push) مستودعك المحلي مع مستودع GitHub البعيد باستخدام الأمر التالي: git push origin main يجب أن تكون قادرًا عند اكتمال هذه العملية على العودة إلى صفحة غيت حيث أنشأت مستودعك، وتحديث الصفحة، ويجب ملاحظة رفع كامل التطبيق. يمكنك الاستمرار في تحديث مستودعك مع تغير الملفات باستخدام دورة أوامر الإضافة add والتثبيت commit والدفع push. ملاحظة: يمكنك الآن إنشاء نسخة احتياطية من شيفرة مشروعك البرمجية، إذ يمكن أن تكون بعض التغييرات التي سنجريها في الأقسام التالية مفيدة للنشر (أو للتطوير) على أي خدمة استضافة، ويمكن أن تكون بعض التغييرات الأخرى غير مفيدة. أفضل طريقة لذلك هي استخدام git لإدارة الإصدارات، إذ يمكنك باستخدامه الرجوع إلى إصدار سابق معين، ويمكنك الاحتفاظ به في فرع منفصل عن التغييرات الإنتاجية واختيار أيّ تغييرات للتنقل بين فروع الإنتاج والتطوير. يستحق تعلم غيت Git الجهد المبذول، لذا اطلع على مجموعة مقالات Git في أكاديمية حسوب. يُعَد نسخ ملفاتك في موقع آخر الطريقة الأسهل، ولكن يمكنك استخدام أيّ طريقة تتناسب مع معرفتك لنظام git. تحديث التطبيق ليعمل على Railway سنشرح في هذا القسم التغييرات التي يجب إجراؤها على تطبيق المكتبة المحلية LocalLibrary ليعمل على Railway، ولكن لن تمنعك هذه التغييرات من استخدام الاختبار وسير العمل المحلي الذي تعلمناه مسبقًا. ضبط نسخة Node يحتوي الملف package.json على كل ما تحتاجه Railway للعمل على اعتماديات تطبيقك وتحديد الملف الواجب تشغيله لبدء تشغيل موقعك، ولكن المعلومات المهمة الوحيدة المفقودة من الملف package.json الحالي هي نسخة Node، إذ يمكنك العثور على نسخة Node التي نستخدمها للتطوير من خلال إدخال الأمر التالي: >node --version v16.17.1 افتح الملف package.json وأضِف المعلومات التالية في قسم engines > node (مع استخدام رقم النسخة في نظامك): { "name": "express-locallibrary-tutorial", "version": "0.0.0", "engines": { "node": ">=16.17.1" }, "private": true, // … لاحظ أن هناك طرقًا أخرى لتوفير نسخة Node على Railway، ولكننا نستخدم الملف package.json لأنه الأسلوب الذي تدعمه العديد من الخدمات على نطاق واسع. لاحظ أيضًا أن Railway لن تستخدم بالضرورة نسخة Node الدقيقة التي تحددها، إذ ستستخدم نسخةً لها رقم النسخة الرئيسي نفسه. الحصول على الاعتماديات وإعادة الاختبار لنختبر الموقع مرةً أخرى ونتأكد من أنه لم يتأثر بأيٍّ من التغييرات التي أجريناها. أولًا، يجب جلب الاعتماديات (تذكّر أننا لم ننسخ المجلد node_modules في شجرة git)، إذ يمكنك ذلك من خلال تشغيل الأمر التالي في طرفيتك ضمن جذر المشروع: npm install شغّل الموقع (اطّلع على فقرة اختبار الوجهات في مقال الوجهات والمتحكمات لمعرفة الأوامر ذات الصلة) وتحقق من أن الموقع لا يزال يتصرف كما هو متوقع. حفظ التغييرات على غيت هب GitHub احفظ الآن جميع التغييرات التي أجريتها على غيت هب، وأدخِل الأوامر التالية في الطرفية أثناء وجودك في مستودعك: git add -A git commit -m "Added files and changes required for deployment" git push origin main يجب أن نكون جاهزين الآن لبدء نشر موقع المكتبة المحلية LocalLibrary على Railway. الحصول على حساب على Railway يجب أولًا إنشاء حساب لبدء استخدام Railway باتباع الخطوات التالية: اذهب إلى موقع Railway الرسمي وانقر على ارتباط تسجيل الدخول Login في شريط الأدوات العلوي. اختر GitHub في النافذة المنبثقة لتسجيل الدخول باستخدام اعتماديات GitHub الخاصة بك. يمكن أن تحتاج بعد ذلك إلى الانتقال إلى بريدك الإلكتروني والتحقق من حسابك. ستسجل بعد ذلك الدخول إلى لوحة تحكم Railway. النشر على Railway من GitHub يجب الآن إعداد Railway لنشر موقع مكتبتنا من GitHub، لذا اختر أولًا خيار لوحة التحكم Dashboard من القائمة العلوية للموقع، ثم حدّد زر مشروع جديد New Project: ستعرض Railway قائمةً بالخيارات الخاصة بالمشروع الجديد، بما في ذلك خيار نشر مشروع من قالب أنشأته لأول مرة في حسابك على GitHub، وعددًا من قواعد البيانات، لذا حدد خيار النشر Deploy from GitHub repo. ستُعرَض جميع المشاريع في مستودعات GitHub التي شاركتها مع Railway أثناء عملية الإعداد، ولكنك ستختار مستودع GitHub الخاص بموقع المكتبة المحلية: <user-name>/express-locallibrary-tutorial. أكّد النشر من خلال تحديد خيار النشر حالًا Deploy Now. ستحمّل Railway بعد ذلك مشروعك وتنشره، مع عرض التقدم في نافذة عمليات النشر Deployments، ثم سترى شيئًا يشبه ما يلي عند اكتمال النشر بنجاح: اختر الآن نافذة الإعدادات Settings، ثم انتقل إلى الأسفل إلى قسم النطاقات Domains، واضغط على زر توليد نطاق Generate Domain. سيؤدي ذلك إلى نشر الموقع ووضع النطاق في مكان الزر كما هو موضح فيما يلي: حدّد عنوان URL للنطاق لفتح تطبيق المكتبة. لاحظ فتح موقع المكتبة المحلية باستخدام بيانات التطوير الخاصة بك لأننا لم نحدّد قاعدة بيانات الإنتاج. تجهيز وتوصيل قاعدة بيانات MongoDb لننشئ الآن قاعدة بيانات MongoDB الخاصة بالإنتاج لاستخدامها بدلًا من استخدام بيانات التطوير، إذ سننشئ قاعدة البيانات بوصفها جزءًا من مشروع تطبيق Railway، بالرغم من أنه لا يوجد ما يمنعك من إنشائها في مشروعها المنفصل أو استخدام قاعدة بيانات MongoDB Atlas لبيانات الإنتاج، كما فعلت مع قاعدة بيانات التطوير. حدّد الخيار Dashboard من قائمة الموقع العلوية على Railway ثم حدد مشروع تطبيقك، الذي يحتوي حاليًا على خدمة واحدة فقط لتطبيقك، والتي يمكن اختيارها لضبط المتغيرات وتفاصيل الخدمة الأخرى، ثم حدّد الزر جديد New، والذي يستخدم لإضافة خدمات إلى المشروع الحالي. حدد قاعدة البيانات Database عندما يُطلَب منك تحديد نوع الخدمة لإضافتها. حدّد بعد ذلك خيار الإضافة Add MongoDB لبدء إضافة قاعدة البيانات. ستجهز Railway بعد ذلك خدمةً تحتوي على قاعدة بيانات فارغة في المشروع نفسه، وسترى عند الانتهاء كلًا من التطبيق وخدمات قاعدة البيانات في عرض المشروع. حدّد خدمة MongoDB لعرض معلومات حول قاعدة البيانات، ثم افتح نافذة "الاتصال Connect" وانسخ "عنوان URL لاتصال Mongo"، وهو عنوان قاعدة البيانات. يمكن جعل تطبيق المكتبة يصل إلى هذا العنوان من خلال إضافته إلى عملية التطبيق باستخدام متغير بيئة، لذا افتح أولًا خدمة التطبيق، ثم حدّد نافذة المتغيرات Variables واضغط على زر متغير جديد New Variable. أدخِل اسم المتغير MONGODB_URI وعنوان URL للاتصال الذي نسخته لقاعدة البيانات، فالمتغير MONGODB_URI هو اسم متغير البيئة الذي ضبطنا التطبيق منه لقراءة عنوان قاعدة البيانات، وسيظهر لديك ما يلي: حدّد بعد ذلك زر الإضافة Add لإضافة المتغير. تعيد Railway تشغيل تطبيقك عندما تحدّث المتغيرات. إذا فحصتَ الصفحة الرئيسية الآن، فيجب أن تظهِر قيمًا صفرية لأعداد الكائنات، إذ تعني التغييرات السابقة أننا نستخدم الآن قاعدة بيانات جديدة (فارغة). متغيرات الضبط الأخرى تذكّر من القسم السابق أننا بحاجة إلى ضبط NODE_ENV على قيمة الإنتاج 'production' لتحسين الأداء وإنشاء رسائل خطأ أقل تفصيلًا، ويمكننا فعل ذلك في الشاشة نفسها التي ضبطنا منها المتغير MONGODB_URI. افتح خدمة التطبيق، ثم حدد نافذة المتغيرات Variables، إذ سترى أن المتغير MONGODB_URI مُعرَّف مسبقًا، واضغط على زر متغير جديد New Variable. أدخل NODE_ENV بوصفه اسم المتغير الجديد و production بوصفه اسم البيئة، ثم اضغط على زر الإضافة Add. ضُبِط وأُعِدّ تطبيق المكتبة المحلية للاستخدام في بيئة الإنتاج، ويمكنك إضافة البيانات عبر واجهة موقع الويب ويجب أن تعمل بالطريقة نفسها التي كانت تعمل بها أثناء عملية التطوير (مع وصول أقل إلى معلومات تنقيح الأخطاء للصفحات غير الصالحة). ملاحظة: إذا أدرتَ إضافة بعض البيانات للاختبار، فيمكنك استخدام السكربت populatedb مع عنوان URL لقاعدة بيانات MongoDB الخاصة بالإنتاج كما ناقشنا في مقال استخدام قاعدة البيانات باستخدام مكتبة Mongoose. تثبيت العميل نزّل وثبّت عميل Railway لنظام تشغيلك المحلي باتباع التعليمات الواردة في توثيق Railway. ستتمكن من تشغيل الأوامر بعد تثبيت العميل. تتضمن بعض العمليات الأكثر أهمية نشر المجلد الحالي لحاسوبك إلى مشروع Railway المرتبط به دون الحاجة للرفع على GitHub، وتشغيل مشروعك محليًا باستخدام الإعدادات نفسها الموجودة على خادم الإنتاج. يمكنك الحصول على قائمة بجميع الأوامر الممكنة من خلال إدخال الأمر التالي في الطرفية: railway help تنقيح الأخطاء Debugging يوفّر عميل Railway الأمر logs لإظهار السجلات الأخيرة (يتوفر سجل كامل على الموقع لكل مشروع): railway logs الخلاصة وصلنا إلى نهاية مقالنا حول إعداد تطبيقات Express في بيئة الإنتاج، وكذلك إلى نهاية سلسلة مقالات إطار عمل Express، لذا نأمل أنها كانت مفيدة لك، ولا تنسَ أنه يمكنك التحقق من النسخة الكاملة من الشيفرة المصدرية على GitHub. ترجمة -وبتصرُّف- للمقال Express Tutorial Part 7: Deploying to production. اقرأ المزيد المقال السابق تطبيق عملي لتعلم Express - الجزء الرابع: عرض بيانات المكتبة والعمل مع الاستمارات. مرحلة نشر التطبيق في عملية تطوير الويب. تطبيق عملي لتعلم جانغو - الجزء 10: نشر تطبيق جانغو في بيئة الإنتاج.
-
أصبحنا الآن جاهزين لإضافة الصفحات التي تعرض كتب موقع المكتبة المحلية LocalLibrary وبيانات أخرى، إذ ستتضمن هذه الصفحات صفحةً رئيسية توضح عدد السجلات لكل نوع نموذج Model وعددًا من صفحات القائمة والصفحات التفصيلية لجميع النماذج، وبالتالي سنكتسب خبرةً عملية في الحصول على السجلات من قاعدة البيانات واستخدام القوالب. سنشرح أيضًا في هذا المقال كيفية العمل مع استمارات HTML في إطار عمل Express باستخدام لغة القوالب Pug، إذ سنناقش كيفية كتابة استمارات لإنشاء المستندات وتحديثها وحذفها من قاعدة بيانات الموقع. المتطلبات الأساسية: إكمال المقالات السابقة من هذه السلسلة بما في ذلك مقال الوِجهات Routes والمتحكمات Controllers. الهدف: فهم كيفية إجراء عمليات قاعدة البيانات غير المتزامنة باستخدام async/await، واستخدام لغة القوالب Pug، والحصول على البيانات من عنوان URL في دوال المتحكمات، وكتابة الاستمارات للحصول على البيانات من المستخدمين وتحديث قاعدة البيانات باستخدام هذه البيانات. عرض بيانات المكتبة عرّفنا في المقالات السابقة نماذجَ Mongoose التي يمكننا استخدامها للتفاعل مع قاعدة بيانات وأنشأنا بعض سجلات المكتبة الأولية، ثم أنشأنا جميع الوِجهات Routes اللازمة لموقع المكتبة المحلية LocalLibrary، ولكن مع دوال متحكمات وهمية dummy، وهي دوال متحكمات هيكلية تعيد فقط رسالة "not implemented" عند الوصول إلى الصفحة. تتمثل الخطوة التالية في توفير عمليات تقديم مناسبة للصفحات التي تعرض معلومات المكتبة، إذ سنتعرّف على تقديم الصفحات التي تعرض استمارات لإنشاء المعلومات أو تحديثها أو حذفها، ويتضمن ذلك تحديث دوال المتحكمات لجلب السجلات باستخدام النماذج وتعريف القوالب لعرض هذه المعلومات للمستخدمين. سنبدأ بتقديم موضوعات عامة أو أولية توضح كيفية إدارة العمليات غير المتزامنة في دوال المتحكمات وكيفية كتابة القوالب باستخدام مكتبة القوالب Pug، ثم سنوفّر عمليات تقديم لكل صفحة من صفحاتنا الرئيسية المُعَدّة للقراءة فقط مع شرح موجز لأي ميزات خاصة أو جديدة تستخدمها، وبالتالي يجب أن يكون لديك فهم جيد وشامل لكيفية عمل الوجهات والدوال غير المتزامنة والعروض Views والنماذج عمليًا. تمر المواضيع التالية بعملية إضافة الميزات المختلفة المطلوبة لعرض صفحات الموقع المطلوبة، إذ يجب أن تطبّق كل قسم منها عمليًا قبل الانتقال إلى القسم التالي: مقدمة إلى القوالب Template، وإنشاء القالب الأساسي لموقع مكتبة محلية مثالًا. إنشاء الصفحة الرئيسية وصفحات القوائم لموقع المكتبة المحلية التي تتضمن: صفحة قائمة الكتب. صفحة قائمة نسخ الكتب BookInstance. تنسيق التاريخ باستخدام مكتبة Luxon. صفحة قائمة المؤلفين وتحدي صفحة قائمة أنواع الكتب. إنشاء صفحات التفاصيل لموقع المكتبة المحلية التي تتضمن: صفحة تفاصيل نوع الكتاب. صفحة تفاصيل الكتاب. صفحة تفاصيل المؤلف. صفحة تفاصيل نسخ الكتاب والتحدي. أنشأنا جميع الصفحات المُعَدّة للقراءة فقط الخاصة بموقعنا وهي: صفحة رئيسية تعرض عدد نسخ كلّ نموذج من نماذجنا، وصفحات قائمة وتفاصيل الكتب ونسخ الكتب والمؤلفين وأنواع الكتب، واكتسبنا الكثير من المعرفة الأساسية حول المتحكمات وإدارة التحكم في التدفق عند استخدام العمليات غير المتزامنة، وإنشاء عروض باستخدام مكتبة القوالب Pug، والاستعلام في قاعدة بيانات الموقع باستخدام النماذج، وتمرير المعلومات إلى العرض، وإنشاء القوالب وتوسيعها، وستعلّمك التحديات أيضًا عن معالجة التاريخ باستخدام مكتبة Luxon. سننشئ الآن استمارات HTML وشيفرة معالجة الاستمارات لبدء تعديل البيانات التي يخزنها الموقع. العمل مع الاستمارات استمارة HTML هي مجموعة مؤلفة من حقل أو عنصر واجهة مستخدم واحد أو أكثر على صفحة الويب، والتي يمكن استخدامها لجمع المعلومات من المستخدمين لإرسالها إلى الخادم، وتُعَد آليةً مرنة لتجميع مدخلات المستخدم نظرًا لوجود مدخلات مناسبة في الاستمارة التي تكون متاحة لإدخال العديد من أنواع البيانات المختلفة مثل مربعات النص ومربعات الاختيار وأزرار الاختيار ومنتقيات التاريخ وما إلى ذلك. تُعَد الاستمارات أيضًا طريقةً آمنة نسبيًا لمشاركة البيانات مع الخادم، لأنها تسمح بإرسال البيانات في طلبات POST مع الحماية من هجمات طلبات التزوير عبر المواقع. يمكن أن يكون التعامل مع الاستمارات معقدًا، إذ يحتاج المطورون إلى كتابة شيفرة HTML للاستمارة، والتحقق من صحة البيانات المُدخَلة وتطهيرها Sanitize بصورة صحيحة على الخادم وربما في المتصفح أيضًا، وإعادة نشر الاستمارة مع رسائل خطأ لإعلام المستخدمين بأي حقول غير صالحة، والتعامل مع البيانات عند إرسالها بنجاح، وأخيرًا الرد على المستخدم بطريقةٍ ما للإشارة إلى النجاح. سنوضح في هذا المقال كيفية تنفيذ هذه العمليات في إطار عمل Express، إذ سنوسّع موقع المكتبة المحلية LocalLibrary للسماح للمستخدمين بإنشاء عناصر وتعديلها وحذفها من المكتبة. ملاحظة: لم نتطرق إلى كيفية تقييد وجهات Routes معينة للمستخدمين المستوثقين أو المُصرَّح لهم، لذلك سيتمكن أيّ مستخدم من إجراء تغييرات على قاعدة البيانات. استمارات HTML اطّلع أولًا على نظرة عامة موجزة على استمارات HTML. ليكن لدينا استمارة HTML البسيطة التالية مع حقل نص واحد لإدخال اسم "الفريق" والتسمية Label المرتبطة به: تُعرَّف الاستمارات في لغة HTML بوصفها مجموعة من العناصر ضمن وسوم <form>…</form> التي تحتوي على عنصر إدخال input واحد على الأقل من النوع type="submit". <form action="/team_name_url/" method="post"> <label for="team_name">Enter name: </label> <input id="team_name" type="text" name="name_field" value="Default name for team." /> <input type="submit" value="OK" /> </form> ضمّنا في المثال السابق حقلًا نصيًا واحدًا فقط لإدخال اسم الفريق، ولكن يمكن أن تحتوي الاستمارة على أيّ عدد من عناصر الإدخال الأخرى والتسميات Labels المرتبطة بها. تعرّف السمة type الخاصة بالحقل نوع عنصر واجهة المستخدم الذي سيُعرَض، وتُستخدَم سمات الاسم name والمعرّف id الخاصة بالحقل لتعريف الحقل في شيفرة جافا سكريبت Javascript أو CSS أو HTML، بينما تعرّف السمة value القيمة الأولية للحقل عند عرضه لأول مرة. تُحدَّد تسمية الفريق المطابقة باستخدام الوسم label (مثل التسمية "Enter name" السابقة)، مع حقل for الذي يحتوي على قيمة معرّف id لحقل الإدخال input المرتبط به. يُعرَض حقل الإدخال submit بوصفه زرًا افتراضيًا، إذ يمكن للمستخدم الضغط عليه لرفع البيانات التي تحتويها عناصر الإدخال الأخرى إلى الخادم، وهي اسم الفريق team_name فقط في مثالنا. تعرّف سمات الاستمارة تابع HTTP وهو method المستخدَم لإرسال البيانات وهدف البيانات على الخادم (السمة action) كما يلي: action: هو المورد أو عنوان URL، إذ ستُرسَل البيانات للمعالجة عند إرسال الاستمارة. إن لم تُضبَط هذه السمة، أو ضُبِطت بوصفها سلسلة نصية فارغة، فستُرسَل الاستمارة إلى عنوان URL للصفحة الحالية. method: تابع HTTP المُستخدَم لإرسال البيانات: إما POST أو GET. يجب دائمًا استخدام تابع POST إذا أدّت البيانات إلى تغييرٍ في قاعدة بيانات الخادم، لأنه يسمح بمقاومة أكبر لهجمات طلبات التزوير عبر المواقع. يجب استخدام تابع GET فقط للاستمارات التي لا تغير بيانات المستخدم (مثل استمارة البحث)، ويوصَى به عندما تريد أن تكون قادرًا على وضع إشارة مرجعية على عنوان URL أو مشاركته. عملية معالجة الاستمارة تستخدم معالجة الاستمارة الأساليب نفسها التي تعلمناها لعرض معلومات النماذج Models، إذ ترسِل الوِجهة طلبنا إلى دالة متحكم تطبّق أيّ إجراءات قاعدة بيانات مطلوبة بما في ذلك قراءة البيانات من النماذج، ثم تولّد صفحة HTML وتعيدها، ولكن ما يعقّد الأمور هو أن الخادم يحتاج أيضًا إلى أن يكون قادرًا على معالجة البيانات التي يقدّمها المستخدم، وإعادة عرض الاستمارة مع معلومات الخطأ عند وجود مشاكل. يوضّح المخطط التالي عملية معالجة طلبات الاستمارة، بدءًا من طلب الصفحة التي تحتوي على استمارة (كما هو موضّح باللون الأخضر): الأشياء الرئيسية التي يجب أن تطبقها شيفرة معالجة الاستمارة هي: عرض الاستمارة الافتراضية في المرة الأولى التي يطلبها المستخدم. يمكن أن تحتوي الاستمارة على حقول فارغة (إذا أنشأتَ سجلًا جديدًا مثلًا)، أو يمكن ملؤها مسبقًا بالقيم الأولية (إذا غيّرتَ سجلًا أو كان لديك قيم أولية افتراضية مفيدة مثلًا). تلقي البيانات التي يرسلها المستخدم في طلب HTTP من النوع POST مثلًا. التحقق من صحة البيانات وتطهيرها. إذا كان هناك بيانات غير صالحة، فأعِد عرض الاستمارة مع أيّ قيم يملأها المستخدم ورسائل خطأ للحقول التي تحتوي على مشاكل. إذا كانت جميع البيانات صالحة، فطبّق الإجراءات المطلوبة، مثل حفظ البيانات في قاعدة البيانات وإرسال إشعار بالبريد الإلكتروني وإعادة نتيجة البحث وتحميل ملف وإلخ. إعادة توجيه المستخدم إلى صفحة أخرى بعد اكتمال جميع الإجراءات. تُقدَّم شيفرة معالجة الاستمارة غالبًا باستخدام وِجهة GET للعرض الأولي للاستمارة ووجهة POST إلى المسار نفسه للتعامل مع التحقق من صحة بيانات الاستمارة ومعالجتها، إذ سنستخدم هذا الأسلوب في هذا المقال. لا يوفّر إطار عمل Express أيّ دعم لعمليات معالجة الاستمارة، ولكن يمكنه استخدام البرمجيات الوسيطة لمعالجة معاملات POST و GET من الاستمارة وللتحقق من صحة أو تطهير قيمها. التحقق من صحة البيانات وتطهيرها يجب التحقق من صحة البيانات الواردة من النموذج وتطهيرها قبل تخزينها كما يلي: تتحقق عملية التحقق من صحة البيانات Validation من أن القيم المُدخَلة مناسبة لكل حقل، أي أنها ضمن المجال الصحيح والتنسيق الصحيح وإلخ وأن هذه القيم متوفرة لجميع الحقول المطلوبة. تزيل أو تستبدل عملية التطهير Sanitization المحارف الموجودة في البيانات التي يمكن أن تُستخدَم لإرسال محتوًى ضار إلى الخادم. سنستخدم في هذا المقال الوحدة express-validator الشائعة لإجراء كلٍّ من التحقق من صحة بيانات الاستمارة وتطهيرها، إذ يمكننا ثثبيتها من خلال تشغيل الأمر التالي في جذر المشروع: npm install express-validator ملاحظة: يوفر دليل وحدة express-validator على غيت هب GitHub نظرة عامة جيدة على واجهة برمجة التطبيقات، لذا نوصيك بقراءته للحصول على فكرة عن جميع إمكانياتها بما في ذلك استخدام التحقق من صحة المخطط Schema Validation وإنشاء أدوات تحقق مخصصة، إذ سنشرح فيما يلي فقط جزءًا مفيدًا لموقع المكتبة المحلية. يمكن استخدام أداة التحقق من صحة البيانات Validator في المتحكمات من خلال تحديد الدوال التي نريد استيرادها من وحدة express-validator كما يلي: const { body, validationResult } = require("express-validator"); هناك العديد من الدوال المتاحة، مما يسمح بفحص وتطهير البيانات الواردة من معاملات الطلب وجسمه وترويساته وملفات تعريف الارتباط cookies وغير ذلك أو جميعها معًا في وقت واحد، إذ سنستخدم في هذا المقال body و validationResult (كما هو مطلوب سابقًا). تُعرَّف الدوال على النحو التالي: أولًا، body([fields, message]) التي تحدّد مجموعة من الحقول في متن الطلب (معامل POST) للتحقق من صحة البيانات و/أو تطهيرها مع رسالة خطأ اختيارية يمكن عرضها إذا فشلت الاختبارات. تتمثل معايير التحقق من صحة البيانات وتطهيرها بسلسلة متعاقبة في تابع body()، فمثلًا يحدّد السطر التالي أولًا أننا نتحقق من حقل "الاسم name" وأن خطأ التحقق من صحة البيانات سيضبط رسالة الخطأ "اسم فارغ Empty name"، ثم نستدعي تابع التطهير trim() لإزالة المسافة من بداية السلسلة ونهايتها، ثم isLength() للتحقق من أن السلسلة النصية الناتجة ليست فارغة. أخيرًا، نستدعي escape() لإزالة محارف HTML من المتغير الذي يمكن استخدامه في هجمات كتابة سكربتات جافا سكربت العابرة للمواقع. [ // … body("name", "Empty name").trim().isLength({ min: 1 }).escape(), // … ]; يتحقق الاختبار التالي من أن حقل العمر age هو تاريخ صالح ويستخدم optional() لتحديد أن السلاسل النصية الخالية والفارغة لن تفشل في التحقق من صحة البيانات. [ // … body("age", "Invalid age") .optional({ values: "falsy" }) .isISO8601() .toDate(), // … ]; يمكنك أيضًا استخدام سلسلة متعاقبة لأدوات التحقق من صحة البيانات المختلفة وإضافة الرسائل التي تُعرَض إذا كانت أدوات التحقق السابقة صحيحة. [ // … body("name") .trim() .isLength({ min: 1 }) .withMessage("Name empty.") .isAlpha() .withMessage("Name must be alphabet letters."), // … ]; ثانيًا، الدالة validationResult(req) التي تشغّل عملية التحقق من صحة البيانات، وتتيح الأخطاء بصيغة كائن النتيجة validation، وتستدعَى في دالة رد نداء منفصلة كما هو موضح فيما يلي: asyncHandler(async (req, res, next) => { // استخراج رسائل التحقق من صحة البيانات من الطلب const errors = validationResult(req); if (!errors.isEmpty()) { // هناك أخطاء، لذا اعرض الاستمارة مرة أخرى مع القيم المُطهَّرة أو رسائل الأخطاء // يمكن إعادة رسائل الخطأ في مصفوفة باستخدام errors.array() } else { // البيانات الواردة من الاستمارة صالحة } }); نستخدم التابع isEmpty() الخاص بنتيجة التحقق من صحة البيانات للتحقق مما إذا كان هناك أخطاء، والتابع array() للحصول على مجموعة رسائل الخطأ (اطلع على قسم معالجة التحقق من صحة البيانات للحصول على مزيد من المعلومات). تُعَد سلاسل التحقق من صحة البيانات وتطهيرها برمجيات وسيطة يجب تمريرها إلى معالج وجهة Express، إذ نطبّق ذلك بطريقة غير مباشرة باستخدام المتحكم، وتعمل أدوات التحقق من صحة البيانات وتطهيرها بالترتيب المُحدَّد عند تشغيل البرمجية الوسيطة. سنغطي بعض الأمثلة الحقيقية عندما نطبق استمارات موقع المكتبة المحلية LocalLibrary. تصميم الاستمارة ترتبط أو تعتمد العديد من النماذج في المكتبة على بعضها بعضًا، إذ يتطلب الكتاب Book مؤلفًا Author ويمكن أن يكون له نوع Genre واحد أو أكثر، مما يؤدي إلى التساؤل حول كيفية التعامل مع الحالة التي يرغب فيها المستخدم في: إنشاء كائن عندما لا تكون الكائنات المرتبطة به موجودةً بعد، مثل كتاب لم يُعرَّف كائن المؤلف فيه. حذف كائن لا يزال كائن آخر يستخدمه مثل حذف كائن النوع Genre الذي لا يزال كائن الكتاب Book يستخدمه. سنبسّط العمل في هذا المشروع بالقول إن الاستمارة يمكنها فقط: إنشاء كائن يستخدم كائنات موجودة فعليًا، لذلك يجب على المستخدمين إنشاء النسخ المطلوبة من المؤلف Author ونوع الكتاب Genre قبل محاولة إنشاء أي كائنات Book. حذف كائن إن لم تشِر إليه كائنات أخرى، فمثلًا لن تتمكّن من حذف كتاب Book حتى حذف جميع كائنات BookInstance المرتبطة به. ملاحظة: يمكن أن يسمح لك التقديم الأكثر مرونة بإنشاء كائنات معتمدة على بعضها البعض عند إنشاء كائن جديد، وحذف أيّ كائن في أي وقت من خلال حذف الكائنات المعتمدة عليه أو إزالة المراجع التي تشير إلى الكائن المحذوف من قاعدة البيانات مثلًا. الوجهات Routes سنحتاج إلى وجهتين لهما نمط عنوان URL نفسه لتقديم شيفرة معالجة الاستمارة، إذ تُستخدَم الوجهة الأولى GET لعرض استمارة فارغة جديدة لإنشاء الكائن، وتُستخدَم الوجهة الثانية (POST) للتحقق من صحة البيانات التي أدخلها المستخدم، ثم حفظ المعلومات وإعادة التوجيه إلى صفحة التفاصيل (إذا كانت البيانات صالحة) أو إعادة عرض الاستمارة مع وجود أخطاء (إذا كانت البيانات غير صالحة). أنشأنا الوجهات مسبقًا لجميع صفحات إنشاء النموذج في الملف "/routes/catalog.js" مثل وجهات نوع الكتب التالية: // طلب GET لإنشاء نوع كتاب Genre، إذ يجب أن يأتي قبل الوجهة التي تعرض نوع الكتاب (تستخدم المعرّف id) router.get("/genre/create", genre_controller.genre_create_get); // طلب POST لإنشاء نوع كتاب router.post("/genre/create", genre_controller.genre_create_post); توضّح المواضيع التالية عملية إضافة الاستمارات المطلوبة إلى تطبيقنا، إذ يجب العمل على كل منها ثم الانتقال إلى الموضوع التالي: إنشاء استمارة نوع الكتاب Genre: تعريف صفحة لإنشاء كائنات Genre. إنشاء استمارة المؤلف Author واستمارة الكتاب Book واستمارة نسخة الكتاب BookInstance: تعريف صفحة لإنشاء كائنات Author وتعريف صفحة أو استمارة لإنشاء كائنات Book وتعريف صفحة أو استمارة لإنشاء كائنات BookInstance. حذف استمارة المؤلف Author وتحديث استمارة الكتاب Book: تعريف صفحة لحذف كائنات Author وتعريف صفحة لتحديث كائنات Book. تحدى نفسك طبّق صفحات الحذف لنماذج Book و BookInstance و Genre واربطها بصفحات التفاصيل المتعلقة بها باستخدام الطريقة نفسها لصفحة حذف المؤلف، ويجب أن تتبع الصفحات أسلوب التصميم نفسه بحيث: إذا كان هناك مراجع إلى كائن من كائنات أخرى، فيجب عرض هذه الكائنات الأخرى مع ملاحظة أنه لا يمكن حذف هذا السجل حتى حذف الكائنات. إن لم يكن هناك مراجع أخرى إلى الكائن، فيجب أن يطالب العرض بحذفه، وإذا ضغط المستخدم على زر الحذف، فيجب حذف السجل. إليك بعض النصائح لتطبيقها: يشبه حذفُ الكائن Genre حذفَ الكائن Author تمامًا، فكلا الكائنين يعتمد عليهما الكائن Book، لذلك لا يمكنك حذف الكائن إلّا عند حذف الكتب المرتبطة به في كلتا الحالتين. يحدث الشيء نفسه عند حذف الكائن Book أيضًا، إذ يجب التحقق أولًا من عدم وجود كائنات BookInstance مرتبطة به. يُعَد حذف الكائن BookInstance أسهل ما في الأمر لأنه لا توجد كائنات معتمدة عليه، إذ يمكنك في هذه الحالة العثور على السجل وحذفه. طبّق صفحات التحديث لنماذج BookInstance و Author و Genre، واربطها بصفحات التفاصيل المرتبطة بها بالطريقة نفسها لصفحة تحديث الكتاب. واتبع أيضًا النصائح التالية: تُعَد صفحة تحديث الكتاب التي طبّقناها للتو هي الأصعب، ويمكن استخدام الأنماط نفسها لصفحات التحديث للكائنات الأخرى. يُعَد حقل تاريخ وفاة المؤلف Author وتاريخ ميلاده وحقل تاريخ استرجاع نسخة الكتاب BookInstance تنسيقًا خاطئًا لإدخاله في حقل إدخال التاريخ في الاستمارة، إذ يتطلب بياناتٍ في الاستمارة بالتنسيق: "YYYY-MM-DD". أسهل طريقة للتغلب على ذلك هي تعريف خاصية افتراضية جديدة للتواريخ التي تنسّق التواريخ بصورة مناسبة، ثم استخدام هذا الحقل في قوالب العرض المرتبطة به. إذا واجهتك مشكلة، فهناك أمثلة لصفحات التحديث يمكنك الاطلاع عليها. الخلاصة توفّر حزم Express و node والحزم الخارجية لمدير حزم npm كل ما تحتاجه لإضافة استمارات إلى موقع الويب، إذ تعلمت في هذا المقال كيفية إنشاء استمارات باستخدام Pug والتحقق من صحة بيانات الإدخال وتطهيرها باستخدام express-validator وإضافة السجلات وحذفها وتعديلها في قاعدة البيانات، ويجب أن تفهم الآن كيفية إضافة الاستمارات الأساسية وشيفرة معالجة الاستمارات إلى مواقع Node الخاصة بك. ترجمة -وبتصرُّف- للمقالين Express Tutorial Part 5: Displaying library data و Express Tutorial Part 6: Working with forms. اقرأ أيضًا المقال السابق تطبيق عملي لتعلم Express - الجزء الثالث: الوجهات Routes والمتحكمات Controllers. الاستمارات (forms) في متصفح الويب وكيفية التعامل معها في جافاسكربت. اإرسال الاستمارات (form submit) ومعالجتها في جافاسكربت.
-
سنوضح في هذا المقال الأدوات الرياضية المُستخدَمة في الرسوميات الحاسوبية ثلاثية الأبعاد Three Dimensional Computer Graphics -أو 3D Graphics اختصارًا، إذ يبني برنامج الرسوميات ثلاثية الأبعاد مشهدًا مكونًا من كائنات ثلاثية الأبعاد في فضاء ثلاثي الأبعاد، ثم تُنتَج صور ثنائية الأبعاد من المشهد ثلاثي الأبعاد، ويُمثَّل المشهد بوصفه هيكل بيانات في ذاكرة الحاسوب، وتُعَد الصورة ثنائية الأبعاد عادةً هي الصورة الموجودة على واجهة شاشة الحاسوب. تتكون الكائنات ثلاثية الأبعاد من نقاط وخطوط ومضلعات، إذ لا بد أنك تعرف ماهية هذه المفاهيم، ولكننا سنوضح في هذا المقال هذه المفاهيم ونقدم بعض المفردات الخاصة بالرسوميات الحاسوبية حتى نبدأ من أرضية مشتركة، إذ سنتعرّف على المواضيع التالية: النقاط. الخطوط. المثلثات. تمثيل النقاط والخطوط. إطارات الإحداثيات. تحديد الإحداثيات. تأثير تغيير الإطارات على الإحداثيات. الأشعة. تمثيل الأشعة. هناك شيئان أساسيان في الهندسة هما النقاط والخطوط، إذًا لنتعرف على هذين المفهومين، ولكن لنبدأ بالإجابة على سؤال "ما هي النقطة؟". النقطة ما هي النقطة؟ النقطة هي موقع في الفضاء. النقطة الهندسية هي موقع في الفضاء بدون أيّ خصائص أخرى، إذ لا تملك طولًا أو عرضًا أو سماكة، وتعَد موقعًا مجردًا. لن يساعدك هذا التعريف لمفهوم النقطة إن لم يكن لديك فهمٌ مسبق له، إذ تشير عبارة "موقع في الفضاء" إلى مفهوم كلمة "نقطة" نفسه، ولا يمكن للتعريف أن ينقل لك مفهومًا لا تملكه مسبقًا. لا يوجد تعريف لكلمة نقطة في الهندسة، فالنقطة هي إحدى المفاهيم الأولية غير المُعرَّفة التي تُستخدَم لتعريف الكائنات الأخرى، إذ تعطي الكتب أمثلةً عنها وتأمل أن تبني بنفسك مفهومًا منها بطريقةٍ ما بدلًا من إعطائك تعريفًا لها. تُظهر الصورة التالية قبتين على أحد المباني، وتُعَد هذه الصورة ثنائية الأبعاد، لكن فكّر في المبنى الفعلي ثلاثيّ الأبعاد، وركّز على القضيب المعدني (النهائي) الموجود أعلى القبة الأقرب. إذًا، تحدّد النهايةُ الحادة للقضيب المعدني موقعًا دقيقًا بحسب مقياس المبنى، إذ يمكننا تصور هذا الموقع بوصفه نقطة. ما هي النقاط فيزيائيا؟ لنفترض أن هذا القضيب المعدني أمامك على مكتبك، فهل تُعَد نهايته نقطة؟ غالبًا لا، إذ تكون النهاية غير حادة جدًا بحسب هذا المقياس، بحيث لا يمكن عدّها نقطة. تعبّر النقطة عن المثالية، إذ تكون نهاية القضيب المعدني بحسب مقياس المبنى صغيرةً بما يكفي لعَدّها موقعًا محددًا، وبالتالي يمكن عدّها نقطة، ولكن تكون نهايته غير حادة جدًا بحسب مقياس مكتبك بحيث لا يمكنها تحديد موقع واحد فعلًا. يجب أن يتناقص حجم هذا القضيب المعدني إلى حجم الدبوس بحسب مقياس المكتب، وبالتالي يمكنك أن تحدّد نهايته نقطةً ما. لكن إذا وضعت الدبوس تحت المجهر، فستكون نهايته غير حادة جدًا بالنسبة لهذا المقياس مرة أخرى، وبالتالي يجب أن تتقلص أكثر إلى حجم البكتيريا مثلًا قبل أن تتمكن النهاية من تحديد نقطة ما، لكن أصبحت نهاية القضيب المعدني مرةً أخرى سميكةً جدًا بحسب مقياس البكتيريا بحيث لا يمكنها تحديد الموقع بدقة. إذًا تعبّر النقاط في الفضاء ثلاثي الأبعاد عن المثالية، إذ تكون النهاية الحادة للقضيب المعدني دقيقةً بما يكفي لتكون نقطة بالنسبة لنموذج المبنى الحاسوبي، ولا يلزم تحديد الحواف والمستويات والأشكال الأخرى التي يتكون منها المبنى بدقة أكبر. النقاط في الفضاء ما هو موقع نهاية القضيب المعدني الأبعد في الفضاء؟ انظر إلى الصورة مرةً أخرى وتخيل العلاقة بين نقطتي نهايتي القضيبين المعدنيين، إذ لا توجد إجابة فعلية ضرورية حاليًا. تكون نقاط النهاية للقضيبين المعدنيين في مواقع محدّدة من العالم الحقيقي real world، وسنحدّد هذه المواقع باستخدام إطار إحداثي لاحقًا، ولكن لنفكر الآن فقط في النقاط الموجودة في الفضاء. تضع في برامج الرسوميات ثلاثية الأبعاد نقاطًا وخطوطًا وأشياءً أخرى في الفضاء، ثم تسقطها على صورة ثنائية الأبعاد، ويحاكي ذلك ما حدث عند إنتاج الصورة باستخدام الكاميرا، إذ تكون نهايات القضيبين المعدنيين فيزيائيًا نقاطًا في الفضاء ثلاثي الأبعاد. تسقط العدسة كامل المشهد على مستشعر الصور ثنائية الأبعاد، وتحدد نهايات القضبان المعدنية في الصورة ثنائية الأبعاد نقطتين ثنائيتي الأبعاد. الخطوط ما المسافة بين النقطتين عند طرفي القضيبين المعدنيين؟ يمكن أن نخمن أن المسافة حوالي 30 مترًا. يكون الحكم على العمق صعبًا في الصورة ثنائية الأبعاد في أغلب الأحيان، إذ تُفقَد معلومات العمق عند إنتاج صورة ثنائية الأبعاد من مشهد ثلاثي الأبعاد (إما مشهد حقيقي أو مشهد في الحاسوب). فكر الآن في القطعة المستقيمة Line Segment الواقعة بين النقطتين، والتي هي المسار المستقيم بين نقطتين بدون سماكة، ولا توجد سوى قطعة مستقيمة واحدة بين النقطتين. إن لم يكن لديك مفهوم مسبق عن "الخط"، فلن يكون التعريف السابق كافيًا لتعلمه، إذ تشير عبارة "المسار المستقيم" إلى مفهوم الخط الذي عرّفناه. تتعامل غالبًا مع القطع المستقيمة في الرسوميات ثلاثية الأبعاد، بينما تستمر الخطوط بلا نهاية في الهندسة، إذ تكون القطعة المستقيمة جزءًا من الخط بين نقطتين. لا يُعرَّف الخط العالمي في كتب الهندسة عادةً، وهو -مثل النقطة- مفهوم غير مُعرَّف يستخدم لتعريف كائنات أخرى، إذ تقدّم الكتب صورًا وأمثلة عن الخطوط، وتأمل أن تفهم بطريقة أو بأخرى ما هو الخط من خلال النظر إليها. يستخدم بعض الأشخاص مصطلح "خط" بينما يجب عليهم استخدام مصطلح "قطعة مستقيمة" الذي سنستخدمه في هذا المقال، نظرًا لأن القطع المستقيمة شائعة جدًا. القطع المستقيمة هل يمكنك تمديد stretch طول السلك بين النقطتين؟ نعم، من الناحية المفاهيمية على الأقل. يوجد سلك رفيع ممتد بين القضيبين المعدنيين بحسب مقياس المبنى، والذي يعبر عن تقريبٍ لقطعة مستقيمة، وكما تعلم يتعلق هذا الأمر بالمقياس، إذ يُرجَّح أن يكون السلك أسمك من الخط المرسوم بقلم رصاص، فالسلك الموجود على مكتبك سميك جدًا بحيث لا يمكن عدّه قطعة مستقيمة. بينما يكون الخط المرسوم بقلم الرصاص الرفيع والمستقيم رفيعًا بدرجة كافية لعدّه خطًا هندسيًا، وإذا كان سميكًا جدًا، فيمكن أن يكون طول خيط العنكبوت الممتد بين نقطتين مناسبًا. لكن يمكن عَدّ الأسلاك المدروسة على المستوى المعماري بمثابة قطع مستقيمة، ويمكنك بناء نموذج هندسي للمبنى باستخدام القطع المستقيمة لتمثيل الحواف. المثلثات جرّب رسم مثلث يربط بين القضيبين المعدنيين والزاوية العلوية اليسرى من السور الأبيض كما في الصورة التالية: يمكن إنشاء المثلث من خلال ربط ثلاث نقاط في فضاء ثلاثي الأبعاد بقطع مستقيمة، فإذا لم تقع النقاط الثلاث جميعها على الخط نفسه، فستحدّد ثلاث نقاط في الفضاء مثلثًا فريدًا. تُعَد المثلثات الموجودة في الفضاء مهمةً في الرسوميات ثلاثية الأبعاد، إذ تُنشَأ الكائنات ثلاثية الأبعاد التي تشكّل المشهد من المثلثات والمضلعات المسطحة الأخرى. المثلث الموجود في الصورة ثنائية الأبعاد هو إسقاط للمثلث ثلاثي الأبعاد في المشهد، ولكن يشوه الإسقاطُ المثلثَ ثلاثيّ الأبعاد، فالمثلث ثلاثي الأبعاد أكبر بكثير من المثلث ثنائي الأبعاد، وتكون الزوايا مختلفة. لنفترض أن الكائن ثلاثي الأبعاد في المشهد مكونٌ من عدة مثلثات، ولنسقط كل مثلث على صورة ثنائية الأبعاد، والنتيجة هي النسخة المُسقَطة من الكائن ثلاثي الأبعاد. يتكون السور الشبكي الأبيض في المبنى من العديد من المثلثات، وتُظهِر نسخة السور الشبكي الموجودة في الصورة جميع هذه المثلثات التي تسقطها عدسة الكاميرا. التمثيل باستخدام إطار إحداثيات هل تحتاج إلى إطار إحداثي -أي نظام مع تسمية النقاط بالإحداثيات x و y و z- للحديث عن النقاط والخطوط والأشكال المستوية؟ في الحقيقة لا حاجة لذلك. يمكنك التفكير في النقاط والخطوط والأشكال المستوية التي تشكّل المشهد ثلاثي الأبعاد دون استخدام نظام إحداثي، وهذا ما كنا نفعله في الحقيقة، ولكن يجب تمثيل هذه الكائنات بطريقة أو بأخرى والتعامل معها في رسوميات الحاسوب، ولذلك نحتاج إلى طريقة لتمثيلها. اختر نقطةً مناسبة في المشهد ثلاثي الأبعاد وأطلِق عليها اسم نقطة الأصل Origin، وحدد ثلاثة خطوط تسمى X و Y و Z والتي تمر عبر نقطة الأصل، وتكون الخطوط الثلاثة عادةً متعامدة على بعضها بعضًا، ويسمى كل خط محورًا Axis. اختر اتجاهًا إيجابيًا لكل خط، فهناك عدة خيارات لذلك، ولكن لنستخدم الخيار الموضّح في الصورة السابقة. تمثل كل نقطة من المحور X مسافةً فريدةً (موجبة أو سالبة أو صفر) من نقطة الأصل، فمثلًا تقع زاوية المبنى الموجودة على المحور X على مسافة 2.4 متر (8 أقدام) تقريبًا من نقطة الأصل. الإحداثيات تقع نقطة الأصل على ارتفاع صفر، ولكن على أي ارتفاع تقع نقطة نهاية القضيب المعدني الأقرب تقريبًا؟ يمكن القول أن ارتفاعها حوالي 3.6 مترًا (10 أقدام). أو يمكنك القول أن y=3.6 بالنسبة لنقطة نهاية القضيب المعدني، إذ يمكن إسناد تمثيل لأيّ نقطة في مشهدنا ثلاثيّ الأبعاد من خلال قياس المسافة على طول المحاور الثلاثة، إذ سنضع هذه المسافات الثلاث في مصفوفة عمودية على النحو التالي في هذا المقال (وفي العديد من كتب الرسوميات الحاسوبية): قد تكون معتادًا على وضع الإحداثيات الثلاثة في صف ثلاثي مثل (x, y, z)، ولكن سيتبين أنه من الملائم أكثر وضعها في مصفوفة عمودية كما ذكرنا بالنسبة للرسوميات الحاسوبية، فمثلًا إحداثيات زاوية المبنى التي تقع على طول المحور X هي تقريبًا: تحديد الإحداثيات ما هي إحداثيات نقطة زاوية الشبكة البيضاء على طول الحافة العلوية (المسمَّاة P0)؟ يمكن القول أنها تقريبًا: احسب عند تحديد الإحداثيات قياس المسافة على طول الخطوط الموازية لمحاور الإحداثيات، إذ ستشكّل الخطوط التي تستخدمها مستطيلًا أو أوجه صندوق. تشكّل الخطوط السوداء المنقطة المستخدمة للنقطة Q0 مستطيلًا، ويعطي قياس أضلاع المستطيل الإحداثيات التالية: من الصعب معرفة إحداثيات النقطة Q1، فالصورة ثنائية الأبعاد، ولكن المشهد الذي تظهره ثلاثي الأبعاد، وبالتالي فإن المحور Z مشوه، ولكن تشكل الخطوط المنقطة في المشهد ثلاثي الأبعاد صندوقًا، فقياس جميع الزوايا 90 درجة. مزيد من النقاط يمكن تقدير إحداثيات النقطة Q1 بالقيم التالية: لنعُد الآن إلى مشهدنا الواقعي، إذ يبلغ طول الشبكة البيضاء تقريبًا حوالي 2.4 متر (8 أقدام) من كل جانب. النقطة وتمثيلها يمكن تخمين إحداثيات طرف القضيب المعدني الأقرب بالقيم التالية: تُعَد المصفوفة العمودية كائنًا رياضيًا يمثل النقطة باستخدام إطار الإحداثيات الذي اخترناه، ولكن ليست هذه المصفوفة العمودية هي النقطة بحد ذاتها، فالمصفوفة العمودية والنقطة شيئان مختلفان، إذ يُستخدَم أحدهما لتمثيل الآخر مثل استخدام اسمك لتمثيلك. نأمل أن يكون ذلك واضحًا، ولكنه سيكون غامضًا بالنسبة للطلاب الذين يحاولون تخطي مواضيع رياضيات الرسوميات الحاسوبية بسرعة، مما يؤدي إلى ندمهم في نهاية المطاف، إذ يمكن أن يمثل كتاب الرسوميات الحاسوبية النقطة نفسها بعدة طرق مختلفة، لذا من الضروري الفصل بين أفكار النقطة الهندسية والطرق المتعددة التي يمكن تمثيلها باستخدامها. ملاحظة: يتحدث الأشخاص في أغلب الأحيان عرضيًا ويقولون أشياء مثل "النقطة (4, 12, -4)"، كما لو كان هذا الصف الثلاثي من الأرقام متطابقًا مع النقطة، ولكن يُستخدَم ذلك للراحة فقط، إذ ينبغي عليهم أن يقولوا: "النقطة المُمثَّلة في نظام الإحداثيات الذي اخترناه بالتمثيل (4, 12, -4)." تأثير تغيير الإطارات على الإحداثيات هل تقاس المسافات على طول محاور الإحداثيات بوحدة القدم أم بالأمتار؟ لا يهم، طالما أنك تستخدمها بصورة متناسقة. لا تُوضَع إطارات الإحداثيات المستخدمة في كتب الرياضيات في مشهد من العالم الحقيقي عادةً، لذلك لا يُعبَّر عن المسافات بأيّ وحدة معينة، ولكن يتعين عليك تحديد الوحدات التي تستخدمها عندما تنشئ عالمًا ثلاثي الأبعاد في الحاسوب. تُظهر الصورة التالية عالمًا ثلاثيّ الأبعاد مع وضع إطارٍ إحداثي مختلف فيه، إذ تبقى النقاط في العالم نفسها كما كانت سابقًا، ولكن سيكون لديها تمثيلات مختلفة مع إطار إحداثي مختلف: مثلنا النقطة المسمَّاة P1 بما يلي في الإطار السابق: ستكون الآن المسافات على طول المحاور في الإطار الجديد (الأخضر) مختلفة، فالمصفوفة العمودية التي تمثل النقطة P1 في الإطار الجديد هي تقريبًا: لم نمارس الرياضيات بصورة دقيقة هنا، بل نظرنا إلى الصورة وخمّنا المسافات فقط، لذا حاول أن تفعل الشيء نفسه، فالهدف ليس حساب أي شيء، بل التفكير في النقاط في الفضاء. للنقطة نفسها تمثيلات مختلفة في كل إطار، لذا يجب أن تعرف الإطار المُستخدَم عندما تمثل نقطةً بمصفوفة عمودية. ضوء الشمس يمكن تقدير إحداثيات النقطة P0 على الحافة العلوية للشبكة المبينة في الصورة أعلاه بالمصفوفة العمودية التالية: ولكن هذه القيم مجرد تقدير فقط. انظر إلى صورة المبنى مرةً أخرى، إذ تضيء الشمسُ المشهد، ولكنها ليست موجودة في الصورة، إذ يأتي ضوء الشمس في المشهد قطريًا من خارج المشهد إلى يساره. الأشعة Vector ارسم ذهنيًا بعض الخطوط في الصورة التي تظهر ضوء الشمس كما في الشكل التالي: يتدفق الضوء من أعلى اليسار عندما تنظر إلى المبنى، إذ تمثل الصورة ضوء الشمس بأسهم. حاول التفكير في المشهد ثلاثي الأبعاد بالرغم من أن الصورة ثنائية الأبعاد، فالاتجاه الذي يتحرك فيه الضوء موضَّح بالسهم، ويمثل طول السهم مثلًا شدة الضوء. توجد عدة أسهم في الصورة، إلا أنّ أيّ سهمٍ يكفي لتمثيل الضوء وشدته. يمكن تمثيل اتجاه الضوء وشدته بشعاع، والذي هو كائن هندسي له خاصيتان هما: الطول والاتجاه. للضوء الصادر من الشمس خاصيتان، هما: السطوع والاتجاه (مع تجاهل معلومات اللون)، ويتناسب طول الشعاع مع سطوع الضوء. يمكن تمثيل الرياح أيضًا بشعاع، فالرياح لها اتجاه وسرعة، إذ تنتقل ذرة من الغبار مع اتجاه الريح، ويتناسب طول الشعاع مع سرعة ذرة الغبار. ليس للشعاع موضع، إذ تُظهِر الصورة السابقة عدة أسهم تمثل تدفق الضوء القادم من الشمس، ولكن يمكن رسم الأسهم في أيّ مكان طالما أن اتجاهها وطولها هو نفسه. تمثيل الأشعة هل تفترض أن برنامج الرسوميات الحاسوبية يحتاج إلى تمثيل شدة الضوء واتجاهه؟ نعم، في الحقيقة هناك حاجة لذلك. تُستخدَم الأشعة لعدة أغراض في رسوميات الحاسوب ثلاثية الأبعاد، لذلك يجب أن تُمثَّل بطريقة يمكن للبرامج التعامل معها، ويحدث ذلك باستخدام الأرقام. لاحظ أن الصورة التالية تتضمن الآن إطار إحداثياتنا: يُمثَّل الشعاع بمصفوفة عمودية كما هو الحال مع النقاط، ويمكن أن يكون من المربك إلى حدٍ ما أن تُمثَّل النقاط والأشعة باستخدام الشيء نفسه، ولكن ذلك سيثبت أنه هذه الطريقة مناسبة ليتعامل معها الحاسوب. يمكن تمثيل الشعاع بمصفوفة عمودية باتباع الخطوات التالية: اختر إطارًا إحداثيًا. احسب طول X (موجب أو سالب) من ذيل الشعاع إلى رأسه. 3.احسب طول Y (موجب أو سالب) من ذيل الشعاع إلى رأسه. احسب طول Z (موجب أو سالب) من ذيل الشعاع إلى رأسه. ضع هذه الأرقام الثلاثة في عمود. إحدى الطرق الملائمة لذلك هي وضع ذيل الشعاع عند نقطة الأصل، ثم قراءة قيم x و y و z عند رأسه. الأشعة والإطارات كم مترًا في الاتجاه X للمسافة بين رأس الأسهم عن ذيلها في الصورة؟ يمكن أن نخمنها بمقدار حوالي 0.9 متر (3 أقدام). كم مترًا في الاتجاه Y يبعد الرأس عن الذيل؟ يمكن أن نخمنها بمقدار 1.2- متر (-4 أقدام). كم مترًا في الاتجاه Z يبعد الرأس من الذيل؟ يمكن أن نخمنها بمقدار 0.9- متر (-3 أقدام). إذًا، الشعاع الذي يعطي شدة واتجاه ضوء الشمس في الصورة هو: تمثل المصفوفة العمودية السابقة الشعاع باستخدام إطار الإحداثيات الذي اخترناه، ولكنها ليست الشعاع نفسه. إذا اخترنا إطارًا إحداثيًا مختلفًا، فسيكون الشعاع في المشهد الحقيقي هو نفسه، ولكن سيتغير تمثيله باستخدام الإطار الجديد، وسنواصل في المقال التالي هذه المناقشة. مراجعة سريعة: للنقطة خاصية واحدة فقط هي: الموقع. للشعاع خاصيتان، هما: الطول والاتجاه ولكن ليس له موقع. ترجمة -وبتصرُّف- للفصل Points and Lines من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا تعرف على أشهر برامج وتطبيقات تصميم الصور والرسوميات قواعد تصميم الرسوم البيانية قواعد التعامل مع الصور والرسوميات
-
سنُعِدّ في هذا المقال الوجهات Routes (شيفرة معالجة عناوين URL، ويسميها البعض بالموجهات) باستخدام دوال معالجة وهمية dummy لجميع النقاط النهائية للموارد التي سنحتاجها في موقع المكتبة المحلية LocalLibrary. سيكون لدينا في النهاية بنية معيارية لشيفرة معالجة الوجهات، والتي يمكننا توسيعها باستخدام دوال معالجة حقيقية في المقالات اللاحقة، وسيصبح لدينا أيضًا فهم جيد لكيفية إنشاء وجهات معيارية باستخدام إطار عمل Express. المتطلبات الأساسية: الاطلاع على مقال مدخل إلى إطار عمل الويب Express، وإكمال المقالات السابقة من هذه السلسلة بما في ذلك مقال استخدام قاعدة البيانات. الهدف: فهم كيفية إنشاء وجهات بسيطة وإعداد جميع النقاط النهائية لعناوين URL. تعرّفنا في المقال السابق على نماذج Mongoose للتفاعل مع قاعدة البيانات، واستخدمنا سكربتًا مستقلًا لإنشاء بعض سجلات المكتبة الأولية، ويمكننا الآن كتابة الشيفرة البرمجية لتقديم تلك المعلومات للمستخدمين. يجب أولًا تحديد المعلومات التي نريد عرضها في صفحاتنا، ثم تعريف عناوين URL المناسبة لإعادة تلك الموارد، ثم يجب إنشاء الوجهات (معالجات عناوين URL) والعروض Views (القوالب Templates) لعرض تلك الصفحات. يوضح المخطط البياني الآتي التدفق الرئيسي للبيانات والأشياء التي يجب تقديمها عند التعامل مع طلب أو استجابة HTTP، ويُوضّح العروض والوجهات والمتحكّمات التي تمثل الدوال التي تفصل الشيفرة البرمجية لتوجيه الطلبات من هذه الشيفرة التي تعالج الطلبات فعليًا. أنشأنا النماذج مسبقًا، فالأشياء الرئيسية التي يجب إنشاؤها هي: "الوجهات Routes" لتوجيه الطلبات المدعومة وأيّ معلومات مُشفَّرة في عناوين URL للطلب إلى دوال المتحكم المناسبة. دوال المتحكم للحصول على البيانات المطلوبة من النماذج وإنشاء صفحة HTML تعرض البيانات وإعادتها إلى المستخدم لعرضها في المتصفح. العروض (القوالب) التي تستخدمها المتحكمات لتقديم البيانات. يمكن أن يكون لدينا في النهاية صفحات لإظهار القوائم والمعلومات التفصيلية للكتب وأنواعها والمؤلفين ونسخ الكتب، وصفحات لإنشاء السجلات وتحديثها وحذفها. تُعَد هذه المعلومات كثيرة لعرضها في مقال واحد، لذلك سيركز معظم هذا المقال على إعداد الوجهات والمتحكمات لإعادة محتوًى "وهمي"، وسنوسّع توابع المتحكم في مقالاتنا اللاحقة للعمل مع بيانات النموذج. يوفّر القسم الأول التالي نظرةً عامةً موجزة حول كيفية استخدام برمجية Express الوسيطة Router، ثم سنستخدم ذلك في الأقسام اللاحقة عند إعداد وجهات موقع المكتبة المحلية LocalLibrary. مقدمة إلى الوجهات الوجهة هي قسم من شيفرة Express التي تربط بين فعل HTTP، مثل GET و POST و PUT و DELETE وإلخ مع مسار أو نمط URL ودالة تُستدعى لمعالجة هذا النمط. هناك عدة طرق لإنشاء الوجهات، وسنستخدم في هذا المقال البرمجية الوسيطة express.Router لأنها تسمح بتجميع معالجات الوجهات لجزء معين من الموقع مع بعضها البعض والوصول إليها باستخدام بادئة prefix وجهة مشتركة. سنحتفظ بجميع الوجهات المتعلقة بالمكتبة في وحدة "دليل catalog"، وإذا أضفنا وجهات لمعالجة حسابات المستخدمين أو لدوال أخرى، فيمكننا الاحتفاظ بها مُجمَّعةً بصورة منفصلة. ملاحظة: ناقشنا سابقًا وجهات تطبيق Express باختصار في مقال سابق. يشبه استخدام كائن الموجّه Router إلى حدٍ كبير تعريف الوجهات مباشرةً في كائن تطبيق Express باستثناء تقديم دعم أفضل للتقسيم إلى وحدات Modularization (كما سننافش في القسم التالي). يوفر الجزء المتبقي من هذا القسم نظرةً عامة حول كيفية استخدام الكائن Router لتعريف الوجهات. تعريف واستخدام وحدات وجهة منفصلة تقدم الشيفرة البرمجية التالية مثالًا عن كيفية إنشاء وحدة وجهة ثم استخدامها في تطبيق Express. أولًا، ننشئ وجهات لميزة الويكي wiki في وحدة بالاسم "wiki.js"، إذ تستورد هذه الشيفرة البرمجية كائن تطبيق Express، وتستخدمه للحصول على كائن Router، ثم تضيف بعض الوجهات إليه باستخدام التابع get()، ثم تصدّر الوحدةُ الكائنَ Router. // wiki.js: وحدة وجهة ويكي Wiki const express = require("express"); const router = express.Router(); // وجهة الصفحة الرئيسية router.get("/", function (req, res) { res.send("Wiki home page"); }); // وجهة صفحة About router.get("/about", function (req, res) { res.send("About this wiki"); }); module.exports = router; ملاحظة: عرّفنا في الشيفرة السابقة دوال رد النداء Callbacks الخاصة بمعالج الوجهة مباشرةً في دوال وجهة، وسنعرّف في موقع المكتبة المحلية LocalLibrary دوال رد النداء هذه في متحكم وحدة منفصل. يمكن استخدام وحدة وجهة في ملف تطبيقنا الرئيسي من خلال طلب وحدة الوجهة (wiki.js) باستخدام الدالة require()، ثم استدعاء الدالة use() في تطبيق Express لإضافة كائن Router إلى مسار معالجة البرمجية الوسيطة مع تحديد مسار URL الخاص بالويكي "wiki". const wiki = require("./wiki.js"); // … app.use("/wiki", wiki); ويمكن بعد ذلك الوصول إلى الوجهتين المُعرَّفتين في وحدة وجهة wiki من /wiki/ و /wiki/about/. دوال الوجهة تعرّف الوحدة السابقة بعضًا من دوال الوجهة، إذ تُعرَّف الوجهة "about" (التي سنعيد إنتاجها فيما يلي) باستخدام التابع Router.get()، والذي يستجيب فقط لطلبات HTTP من النوع GET. الوسيط الأول لهذا التابع هو مسار URL والوسيط الثاني هو دالة رد نداء تُستدعَى عند تلقي طلب HTTP من النوع GET مع المسار. router.get("/about", function (req, res) { res.send("About this wiki"); }); تأخذ دالة رد النداء ثلاثة وسائط تُسمَّى عادة req و res و next، والتي ستحتوي على كائن طلب HTTP واستجابة HTTP والدالة التالية next في سلسلة البرمجيات الوسيطة. ملاحظة: تُعَد دوال Router برمجيات Express وسيطة، مما يعني أنها يجب إما إكمال (أو الاستجابة) للطلب أو استدعاء الدالة next في السلسلة، إذ نكمل في حالتنا الطلب باستخدام التابع send()، لذلك لا يُستخدَم الوسيط next ونختار عدم تحديده. تأخذ دالة router السابقة دالة رد نداء واحدة، ولكن يمكنك تحديد عدة وسائط لدوال رد النداء أو مصفوفة من دوال رد النداء. تُعَد كل دالة جزءًا من سلسلة البرمجيات الوسيطة، وستُستدعَى بالترتيب نفسه لإضافتها إلى السلسلة (إلّا إذا أكملت الدالة السابقة الطلب). تستدعي دالة رد النداء في مثالنا التابع send() في الاستجابة لإعادة السلسلة النصية "About this wiki" عندما نتلقى طلب GET مع المسار ("/about"). هناك عدد من توابع الاستجابة الأخرى لإنهاء دورة الطلب/الاستجابة، فمثلًا يمكنك استدعاء التابع res.json() لإرسال استجابة JSON أو التابع res.sendFile() لإرسال ملف. يُعَد التابع render() تابع الاستجابة الذي سنستخدمه غالبًا أثناء بناء المكتبة، والذي ينشئ ويعيد ملفات HTML باستخدام القوالب والبيانات (سنتحدث عن ذلك في مقالٍ لاحق). أفعال HTTP تستخدم أمثلة الوجهات السابقة التابع Router.get() للاستجابة على طلبات HTTP من النوع GET مع مسار معين. يوفّر الكائن Router أيضًا توابع وجهة لجميع أفعال HTTP الأخرى، والتي تُستخدَم غالبًا بنفس الطريقة تمامًا ومن هذه التوابع: post() و put() و delete() و options() و trace() و copy() و lock() و mkcol() و move() و purge() و propfind() و proppatch() و unlock() و report() و mkactivity() و checkout() و merge() و m-search() و notify() و subscribe() و unsubscribe() و patch() و search() و connect(). تتصرف الشيفرة البرمجية التالية مثلًا مثل وجهة /about السابقة، ولكنها تستجيب فقط لطلبات HTTP من النوع POST: router.post("/about", (req, res) => { res.send("About this wiki"); }); مسارات الوجهة تعرِّف مساراتُ الوجهة النقاطَ النهائية التي يمكن إجراء الطلبات عندها، فالأمثلة التي رأيناها حتى الآن كانت مجرد سلاسل نصية، واُستخدِمت كما هو مكتوب تمامًا مثل: '/' و '/about' و '/book' و '/any-random.path'. يمكن أن تكون مسارات الوجهة أيضًا أنماطًا من السلاسل النصية التي تستخدم شكلًا من صيغ التعابير النمطية لتعريف أنماط النقاط النهائية التي ستجري مطابقتها. نوضح فيما يلي هذه الصيغة (لاحظ تفسير الشَرطة الواصلة (-) والنقطة (.) حرفيًا باستخدام المسارات المستندة إلى السلاسل النصية): ?: يجب أن تحتوي النقطة النهائية endpoint على 0 أو 1 من المحرف السابق (أو المجموعة)، فمثلًا سيتطابق مسار الوجهة '/ab?cd' مع النقاط النهائية acd أو abcd. +: يجب أن تحتوي النقطة النهائية على واحد أو أكثر من المحرف السابق (أو المجموعة)، فمثلًا سيتطابق مسار الوجهة '/ab+cd' مع النقاط النهائية abcd و abbcd و abbbcd وإلخ. *: يمكن أن تحتوي النقطة النهائية على سلسلة نصية عشوائية في مكان المحرف *، فمثلًا سيتطابق مسار الوجهة '/ab*cd' مع النقاط النهائية abcd و abXcd و abSOMErandomTEXTcd وإلخ. (): تجميع تطابق مجموعة من المحارف لإجراء عملية أخرى عليها، فمثلًا سينفّذ '/ab(cd)?e' تطابق ? على المجموعة (cd)، إذ سيتطابق مع abe و abcde. يمكن أن تكون مسارات الوجهة أيضًا تعابير نمطية Regular Expressions في لغة جافا سكريبت JavaScript، فمثلًا سيتطابق مسار الوجهة التالي مع catfish و dogfish، ولكن لن يتطابق مع catflap و catfishhead وما إلى ذلك. لاحظ أن مسار التعبير النمطي يستخدم صياغة التعابير النمطية، وهي ليست سلسلة نصية بين علامات اقتباس كما في الحالات السابقة. app.get(/.*fish$/, function (req, res) { // … }); ملاحظة: ستستخدم معظم الوجهات في موقع المكتبة المحلية LocalLibrary سلاسلًا نصية ولن تستخدم تعابيرًا نمطية، وسنستخدم أيضًا معاملات الوجهات كما سنناقش في القسم التالي. معاملات الوجهة تُسمَّى معاملات الوجهة بأجزاء Segments عنوان URL المُستخدَمة لالتقاط القيم في مواضع محددة من عنوان URL، إذ تُسبَق هذه المقاطع بنقطتين ثم الاسم (مثل /:your_parameter_name/). تُخزَّن القيم المُلتقَطة في كائن req.params من خلال استخدام أسماء المعاملات بوصفها مفاتيحًا، مثل req.params.your_parameter_name. ليكن لدينا مثلًا عنوان URL مُشفَّر يحتوي على معلومات حول المستخدمين والكتب: http://localhost:3000/users/34/books/8989، إذ يمكننا استخراج هذه المعلومات كما هو موضح فيما يلي باستخدام معاملات مسار userId و bookId: app.get("/users/:userId/books/:bookId", (req, res) => { // الوصول إلى userId باستخدام req.params.userId // الوصول إلى bookId باستخدام req.params.bookId res.send(req.params); }); يجب أن تتكون أسماء معاملات الوجهة من "محارف كلمات"، أي A-Z و a-z و0-9 و _. ملاحظة: ستجري مطابقة عنوان "/book/create" مع وجهة مثل الوجهة /book/:bookId، والتي ستستخرج قيمة "bookId" الخاصة بالإنشاء 'create'. ستُستخدَم الوجهة الأولى التي تطابق عنوان URL الوارد، لذلك إذا أردتَ معالجة عناوين /book/create بصورة منفصلة، فيجب تعريف معالج الوجهة قبل الوجهة /book/:bookId. هذا كل ما تحتاجه لبدء استخدام الوجهات، ولكن يمكنك العثور على مزيد من المعلومات في مقال كيفية تحديد الوجهات وأنواع طلبات HTTP وفي توثيق Express: التوجيه الأساسي ودليل التوجيه. توضح الأقسام التالية كيفية إعداد الوجهات والمتحكمات لموقع المكتبة المحلية. معالجة الأخطاء في دوال الوجهة تحتوي جميع دوال الوجهة الموضحة سابقًا على وسائط req و res التي تمثل الطلب والاستجابة على التوالي. تُستدعَى دوال الوجهة أيضًا مع وسيط ثالث هو next، والذي يمكن استخدامه لتمرير الأخطاء إلى سلسلة برمجيات Express الوسيطة. توضح الشيفرة التالية كيفية إنجاز ذلك باستخدام مثال استعلام قاعدة البيانات الذي يأخذ دالة رد نداء ويعيد إما خطأ err أو بعض النتائج. تُستدعَى next مع خطأ err بوصفه قيمة معاملها الأول عند إعادة خطأ err، وسينتقل هذا الخطأ في النهاية إلى شيفرة معالجة الأخطاء العامة، وتُعاد البيانات المطلوبة ثم تُستخدَم في الاستجابة في حالة النجاح. router.get("/about", (req, res, next) => { About.find({}).exec((err, queryResults) => { if (err) { return next(err); } // اعرض شيئًا ما في حالة النجاح res.render("about_view", { title: "About", list: queryResults }); }); }); معالجة الاستثناءات في دوال الوجهة يوضح القسم السابق كيف يتوقع إطار عمل Express أن تعيد دوال الوجهة أخطاءً، إذ صُمِّم إطار العمل للاستخدام مع الدوال غير المتزامنة التي تأخذ دالة رد نداء (مع خطأ وووسيط النتيجة)، والتي تُستدعَى عند اكتمال العملية. يُعَد ذلك مشكلة لأننا سنجري لاحقًا استعلامات قاعدة بيانات Mongoose التي تستخدم واجهات برمجة تطبيقات مستندة إلى الوعود promise، والتي يمكن أن تؤدي إلى رمي استثناءات في دوال الوجهة بدلًا من إعادة الأخطاء في دالة رد النداء. يمكن أن يعالج إطار العمل الاستثناءات بصورة صحيحة من خلال اكتشافها ثم توجيهها بوصفها أخطاءً كما هو موضح في القسم السابق. ملاحظة: من المتوقع أن يعالج Express 5 التجريبي استثناءات جافا سكريبت بطريقة أصيلة. ليكن لدينا المثال البسيط من القسم السابق مع وجود About.find().exec() بوصفه استعلام قاعدة بيانات يعيد وعدًا، إذ يمكن كتابة دالة الوجهة ضمن كتلة try...catch كما يلي: exports.get("/about", async function (req, res, next) { try { const successfulResult = await About.find({}).exec(); res.render("about_view", { title: "About", list: successfulResult }); } catch (error) { return next(error); } }); يُعَد ذلك كمًا كبيرًا جدًا من الشيفرة البرمجية المتداولة لإضافتها إلى كل دالة، لذا سنستخدم بدلًا من ذلك وحدة express-async-handler التي تعرّف دالة تغليف تخفي كتلة try...catch والشيفرة البرمجية لتوجيه الخطأ، وبالتالي أصبح المثال نفسه الآن بسيطًا جدًا، لأننا نحتاج فقط إلى كتابة شيفرة برمجية للحالة التي نفترض فيها النجاح: // استيراد الوحدة const asyncHandler = require("express-async-handler"); exports.get( "/about", asyncHandler(async (req, res, next) => { const successfulResult = await About.find({}).exec(); res.render("about_view", { title: "About", list: successfulResult }); }), ); الوجهات اللازمة لموقع المكتبة المحلية سنعرض فيما يلي عناوين URL التي سنحتاجها لصفحاتنا، إذ نضع اسم كل نموذج من نماذجنا (الكتاب ونسخة الكتاب ونوع الكتاب والمؤلف) مكان الكائن object، والكائنات objects هي جمع كائن، والمعرّف id هو نسخة فريدة من الحقل (_id) تُعطَى لكل نسخة من نموذج Mongoose افتراضيًا. catalog/: الصفحة الرئيسية أو صفحة الفهرس. catalog/<objects>/: قائمة بجميع الكتب أو نسخها أو أنواعها أو المؤلفين، مثل /catalog/books/ و /catalog/genres/ وغير ذلك. catalog/<object>/<id>: صفحة التفاصيل لكتاب أو نسخة أو نوع أو مؤلف معين مع قيمة الحقل _id المُحدَّدة، مثل /catalog/book/584493c1f4887f06c0e67d37. catalog/<object>/create: استمارة إنشاء كتاب أو نسخة أو نوع أو مؤلف جديد، مثل /catalog/book/create. catalog/<object>/<id>/update: استمارة لتحديث كتاب أو نسخة أو نوع أو مؤلف معين مع قيمة الحقل _id المُحدَّدة، مثل /catalog/book/584493c1f4887f06c0e67d37/update. catalog/<object>/<id>/delete: استمارة لحذف كتاب أو نسخة أو نوع أو مؤلف معين مع قيمة الحقل _id المُحدَّدة، مثل /catalog/book/584493c1f4887f06c0e67d37/delete. لا تشفِّر الصفحة الرئيسية الأولى وصفحات القائمة أيّ معلومات إضافية، فبينما تعتمد النتائج المُعادة على نوع النموذج والمحتوى في قاعدة البيانات، ستبقى الاستعلامات المُنفَّذة للحصول على المعلومات نفسها دائمًا، وبالمثل، ستكون الشيفرة البرمجية المُنفَّذة لإنشاء الكائن نفسها دائمًا. تُستخدَم عناوين URL الأخرى للعمل على نسخةٍ من مستند أو نموذج معين، وتشفّر هوية العنصر في عنوان URL (كما هو موضّح في العنصر <id> سابقًا). سنستخدم معاملات المسار لاستخراج المعلومات المشفرة وتمريرها إلى معالج الوجهة، وسنستخدمها في مقال لاحق لتحديد المعلومات التي نحصل عليها من قاعدة البيانات ديناميكيًا. نحتاج فقط إلى وجهة واحدة لكل مورد من نوع معين من خلال تشفير المعلومات في عنوان URL، مثل استخدام وجهة واحدة للتعامل مع عرض عنصر كتاب واحد. ملاحظة: يسمح إطار عمل Express بإنشاء عناوين URL الخاصة بك بالطريقة التي تريدها، إذ يمكنك تشفير المعلومات في متن عنوان URL أو استخدام معاملات URL من النوع GET (مثل /book/?id=6). يجب أن تظل عناوين URL نظيفة ومنطقية وقابلة للقراءة بغض النظر عن الطريقة التي تستخدمها. سننشئ فيما يلي دوال رد نداء معالجة الوجهة وشيفرة الوجهة البرمجية لجميع عناوين URL السابقة. إنشاء دوال رد النداء لمعالجة الوجهة سننشئ أولًا جميع دوال رد النداء الوهمية أو الهيكلية التي ستُستدعَى لاحقًا قبل أن نعرّف الوجهات، وستُخزَّن دوال رد النداء في وحدات متحكمات منفصلة لكل من Book و BookInstance و Genre و Author، إذ يمكنك استخدام أي بنية ملفات أو وحدات، ولكن تبدو الطريقة التي سنستخدمها مناسبة ودقيقة لهذا المشروع. ابدأ بإنشاء مجلدٍ للمتحكمات في جذر المشروع /controllers، ثم أنشئ ملفات أو وحدات منفصلة للمتحكمات للتعامل مع كل نموذج كما يلي: /express-locallibrary-tutorial //the project root /controllers authorController.js bookController.js bookinstanceController.js genreController.js ستستخدم المتحكمات الوحدة express-async-handler، لذا ثبّتها في المكتبة باستخدام npm قبل المتابعة كما يلي: npm install --save express-async-handler متحكم المؤلف Author افتح الملف "/controllers/authorController.js" واكتب فيه الشيفرة البرمجية التالية: const Author = require("../models/author"); const asyncHandler = require("express-async-handler"); // عرض قائمة بجميع المؤلفين exports.author_list = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Author list"); }); // عرض صفحة التفاصيل لمؤلف معين exports.author_detail = asyncHandler(async (req, res, next) => { res.send(`NOT IMPLEMENTED: Author detail: ${req.params.id}`); }); // عرض استمارة إنشاء مؤلف باستخدام GET exports.author_create_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Author create GET"); }); // معالجة إنشاء مؤلف باستخدام POST exports.author_create_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Author create POST"); }); // عرض استمارة حذف المؤلف باستخدام GET exports.author_delete_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Author delete GET"); }); // معالجة حذف المؤلف باستخدام POST exports.author_delete_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Author delete POST"); }); // عرض استمارة تحديث المؤلف باستخدام GET exports.author_update_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Author update GET"); }); // معالجة تحديث المؤلف باستخدام POST exports.author_update_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Author update POST"); }); تطلب الوحدة أولًا نموذج المؤلف Author الذي سنستخدمه لاحقًا للوصول إلى بياناتنا وتحديثها، والمغلِّف asyncHandler الذي سنستخدمه لالتقاط الاستثناءات من دوال معالج الوجهة، ثم تصدِّر دوالًا لكل عنوان من عناوين URL التي نرغب في معالجتها. لاحظ أن عمليات الإنشاء والتحديث والحذف تستخدم الاستمارات Forms، وبالتالي تملك توابعًا إضافية لمعالجة طلبات الاستمارة من النوع Post، وسنناقش هذه التوابع في مقال لاحق. تستخدم جميع الدوال الدالة المُغلِّفة Wrapper السابقة في معالجة الاستثناءات في دوال الوجهة مع وسائط للطلب والاستجابة والدالة التالية next، وتستجيب الدوال بسلسلة نصية تشير إلى أن الصفحة المرتبطة لم تُنشَأ بعد. إذا كان من المتوقع أن تتلقى دالة المتحكم معاملات المسار، فستكون هذه المعاملات هي الخرج الموجود في سلسلة الرسالة النصية (لاحظ req.params.id). لاحظ أنه يمكن ألّا تحتوي بعض دوال الوجهة بعد تقديمها على أي شيفرة برمجية يمكنها أن ترمي استثناءات، ولكن يمكننا تغيير تلك الدوال إلى دوال معالجة وجهة عادية عند الوصول إليها. متحكم نسخة الكتاب BookInstance افتح الملف /controllers/bookinstanceController.js وانسخ الشيفرة البرمجية التالية التي تتبع نمطًا مطابقًا لوحدة المتحكم Author: const BookInstance = require("../models/bookinstance"); const asyncHandler = require("express-async-handler"); // عرض قائمة بجميع نسخ الكتب BookInstance exports.bookinstance_list = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: BookInstance list"); }); // عرض صفحة تفاصيل نسخة كتاب BookInstance معينة exports.bookinstance_detail = asyncHandler(async (req, res, next) => { res.send(`NOT IMPLEMENTED: BookInstance detail: ${req.params.id}`); }); // عرض استمارة إنشاء نسخة الكتاب BookInstance باستخدام GET exports.bookinstance_create_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: BookInstance create GET"); }); // معالجة إنشاء نسخة كتاب BookInstance باستخدام POST exports.bookinstance_create_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: BookInstance create POST"); }); // عرض استمارة حذف نسخة كتاب BookInstance باستخدام GET exports.bookinstance_delete_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: BookInstance delete GET"); }); // معالجة حذف نسخة كتاب BookInstance باستخدام POST exports.bookinstance_delete_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: BookInstance delete POST"); }); // عرض استمارة تحديث نسخة كتاب BookInstance باستخدام GET exports.bookinstance_update_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: BookInstance update GET"); }); // معالجة تحديث نسخة الكتاب باستخدام POST exports.bookinstance_update_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: BookInstance update POST"); }); متحكم نوع الكتاب Genre افتح الملف /controllers/genreController.js والصق فيه النص التالي الذي يتبع نمطًا مطابقًا لملفات Author و BookInstance: const Genre = require("../models/genre"); const asyncHandler = require("express-async-handler"); // عرض قائمة بجميع أنواع الكتب Genre exports.genre_list = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Genre list"); }); // عرض صفحة تفاصيل نوع كتاب Genre معين exports.genre_detail = asyncHandler(async (req, res, next) => { res.send(`NOT IMPLEMENTED: Genre detail: ${req.params.id}`); }); // عرض استمارة إنشاء نوع الكتاب Genre باستخدام GET exports.genre_create_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Genre create GET"); }); // معالجة إنشاء نوع كتاب Genre باستخدام POST exports.genre_create_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Genre create POST"); }); // عرض استمارة حذف نوع كتاب Genre باستخدام GET exports.genre_delete_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Genre delete GET"); }); // معالجة حذف نوع كتاب Genre باستخدام POST exports.genre_delete_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Genre delete POST"); }); // عرض استمارة تحديث نوع كتاب Genre باستخدام GET exports.genre_update_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Genre update GET"); }); // معالجة تحديث نوع كتاب Genre باستخدام POST exports.genre_update_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Genre update POST"); }); متحكم الكتاب Book افتح الملف /controllers/bookController.js وانسخ الشيفرة البرمجية التالية، إذ يتبع هذا الملف النمط المتبع نفسه في وحدات المتحكمات الأخرى، ولكنه يحتوي أيضًا على الدالة index() لعرض صفحة الترحيب بالموقع: const Book = require("../models/book"); const asyncHandler = require("express-async-handler"); exports.index = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Site Home Page"); }); // عرض قائمة بجميع الكتب exports.book_list = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Book list"); }); // عرض صفحة تفاصيل كتاب معين exports.book_detail = asyncHandler(async (req, res, next) => { res.send(`NOT IMPLEMENTED: Book detail: ${req.params.id}`); }); // عرض استمارة إنشاء كتاب باستخدام GET exports.book_create_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Book create GET"); }); // معالجة إنشاء كتاب باستخدام POST exports.book_create_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Book create POST"); }); // عرض استمارة حذف كتاب باستخدام GET exports.book_delete_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Book delete GET"); }); // معالجة حذف كتاب باستخدام POST exports.book_delete_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Book delete POST"); }); // عرض استمارة تحديث كتاب باستخدام GET exports.book_update_get = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Book update GET"); }); // معالجة تحديث كتاب باستخدام POST exports.book_update_post = asyncHandler(async (req, res, next) => { res.send("NOT IMPLEMENTED: Book update POST"); }); إنشاء دليل وحدات الوجهة سننشئ الآن وجهات لجميع عناوين URL التي يحتاجها موقع المكتبة المحلية LocalLibrary، والتي ستستدعي دوال المتحكمات التي عرّفناها في الأقسام السابقة. تحتوي البنية الهيكلية للموقع مسبقًا على المجلد "./routes" الذي يحتوي على وجهات لصفحة الفهرس index والمستخدمين users. أنشئ ملف وجهة آخر هو catalog.js ضمن هذا المجلد كما يلي: /express-locallibrary-tutorial //the project root /routes index.js users.js catalog.js افتح الملف /routes/catalog.js وانسخ الشيفرة البرمجية التالية: const express = require("express"); const router = express.Router(); // طلب وحدات المتحكمات const book_controller = require("../controllers/bookController"); const author_controller = require("../controllers/authorController"); const genre_controller = require("../controllers/genreController"); const book_instance_controller = require("../controllers/bookinstanceController"); /// وجهات الكتاب /// // الحصول على صفحة الدليل الرئيسية router.get("/", book_controller.index); // طلب GET لإنشاء كتاب. يجب أن يأتي هذا الطلب قبل الوجهات Routes التي تعرض الكتاب (يستخدم المعرّف id) router.get("/book/create", book_controller.book_create_get); // طلب POST لإنشاء كتاب router.post("/book/create", book_controller.book_create_post); // طلب GET لحذف كتاب router.get("/book/:id/delete", book_controller.book_delete_get); // طلب POST لحذف كتاب router.post("/book/:id/delete", book_controller.book_delete_post); // طلب GET لتحديث كتاب router.get("/book/:id/update", book_controller.book_update_get); // طلب POST لتحديث كتاب router.post("/book/:id/update", book_controller.book_update_post); // طلب GET لكتاب واحد router.get("/book/:id", book_controller.book_detail); // طلب GET لقائمة جميع عناصر الكتب router.get("/books", book_controller.book_list); /// وجهات المؤلف /// // طلب GET لإنشاء مؤلف Author. يجب أن يأتي هذا الطلب قبل وجهة المعرّف (مثل عرض مؤلف) router.get("/author/create", author_controller.author_create_get); // طلب POST لإنشاء مؤلف router.post("/author/create", author_controller.author_create_post); // طلب GET لحذف مؤلف router.get("/author/:id/delete", author_controller.author_delete_get); // طلب POST لحذف مؤلف router.post("/author/:id/delete", author_controller.author_delete_post); // طلب GET لتحديث مؤلف router.get("/author/:id/update", author_controller.author_update_get); // طلب POST لتحديث مؤلف router.post("/author/:id/update", author_controller.author_update_post); // طلب GET لمؤلف واحد router.get("/author/:id", author_controller.author_detail); // طلب GET لقائمة جميع المؤلفين router.get("/authors", author_controller.author_list); /// وجهات نوع الكتاب GENRE /// // طلب GET لإنشاء نوع كتاب. يجب أن يأتي هذا الطلب قبل الوجهة التي تعرض نوع الكتاب (يستخدم المعرّف) router.get("/genre/create", genre_controller.genre_create_get); // طلب POST لإنشاء نوع كتاب router.post("/genre/create", genre_controller.genre_create_post); // طلب GET لحذف نوع كتاب router.get("/genre/:id/delete", genre_controller.genre_delete_get); // طلب POST لحذف نوع كتاب router.post("/genre/:id/delete", genre_controller.genre_delete_post); // طلب GET لتحديث نوع كتاب router.get("/genre/:id/update", genre_controller.genre_update_get); // طلب POST لتحديث نوع كتاب router.post("/genre/:id/update", genre_controller.genre_update_post); // طلب GET لنوع كتاب واحد router.get("/genre/:id", genre_controller.genre_detail); // طلب GET لقائمة جميع أنواع الكتب router.get("/genres", genre_controller.genre_list); /// طرق نسخ الكتب BOOKINSTANCE /// // طلب GET لإنشاء نسخة كتاب BookInstance. يجب أن يأتي هذا الطلب قبل الوجهة التي تعرض نسخة الكتاب (يستخدم المعرّف) router.get( "/bookinstance/create", book_instance_controller.bookinstance_create_get, ); // طلب POST لإنشاء نسخة كتاب router.post( "/bookinstance/create", book_instance_controller.bookinstance_create_post, ); // طلب GET لحذف نسخة كتاب router.get( "/bookinstance/:id/delete", book_instance_controller.bookinstance_delete_get, ); // طلب POST لحذف نسخة كتاب router.post( "/bookinstance/:id/delete", book_instance_controller.bookinstance_delete_post, ); // طلب GET لتحديث نسخة الكتاب router.get( "/bookinstance/:id/update", book_instance_controller.bookinstance_update_get, ); // طلب POST لتحديث نسخة كتاب router.post( "/bookinstance/:id/update", book_instance_controller.bookinstance_update_post, ); // طلب GET لنسخة كتاب واحدة router.get("/bookinstance/:id", book_instance_controller.bookinstance_detail); // طلب GET لقائمة جميع نسخ الكتاب router.get("/bookinstances", book_instance_controller.bookinstance_list); module.exports = router; تطلب الوحدة كائن Express ثم تستخدمه لإنشاء كائن Router، إذ تُضبَط جميع الوجهات في هذا الكائن الذي يُصدَّر لاحقًا. تُعرّف الوجهات إما باستخدام تابع .get() أو تابع .post() للكائن Router، وتُعرّف جميع المسارات باستخدام سلاسل نصية، إذ لا نستخدم أنماط سلاسل نصية أو تعابيرًا نمطية. تستخدم الوجهات التي تعمل على بعض الموارد المحددة (مثل الكتاب) معاملاتِ المسار للحصول على معرّف الكائن من عنوان URL، وتُستورَد جميع دوال المعالجة من وحدات المتحكمات التي أنشأناها في القسم السابق. تحديث وحدة وجهة صفحة الفهرس ضبطنا جميع وجهاتنا الجديدة، ولكن لا يزال لدينا الوجهة إلى الصفحة الأصلية، ولكن لنعيد التوجيه إلى صفحة الفهرس الجديدة التي أنشأناها في المسار '/catalog' بدلًا من ذلك. افتح الملف /routes/index.js وضع الدالة التالية بدلًا من الوجهة الحالية: // الحصول على الصفحة الرئيسية router.get("/", function (req, res) { res.redirect("/catalog"); }); ملاحظة: استخدمنا تابع الاستجابة redirect() الذي يؤدي إلى إعادة التوجيه إلى الصفحة المحددة، مع إرسال رمز حالة HTTP الذي هو "302 Found" افتراضيًا، ويمكنك تغيير رمز الحالة المُعاد إن لزم الأمر وتوفير مسارات مطلقة أو نسبية. يمكنك الاطلاع على مقال رموز الإجابة في HTTP والمقال كيفية استكشاف وإصلاح رموز أخطاء HTTP الشائعةعلى أكاديمية حسوب لمزيدٍ من المعلومات حول رموز حالة HTTP. تحديث الملف app.js تتمثل الخطوة الأخيرة في إضافة الوجهات إلى سلسلة البرمجيات الوسيطة في الملف app.js، لذا افتح هذا الملف، واطلب وجهة الدليل بعد الوجهات الأخرى من خلال إضافة السطر الثالث التالي بعد السطرين الآخرين الموجودين مسبقًا في الملف: var indexRouter = require("./routes/index"); var usersRouter = require("./routes/users"); const catalogRouter = require("./routes/catalog"); // استيراد الوجهات لمنطقة "الدليل" في الموقع ضِف بعد ذلك وجهة الدليل catalog إلى مكدس البرمجيات الوسيطة بعد الوجهات الأخرى من خلال إضافة السطر الثالث التالي بعد السطرين الآخرين الموجودين مسبقًا في الملف: app.use("/", indexRouter); app.use("/users", usersRouter); app.use("/catalog", catalogRouter); // إضافة وجهات الدليل إلى سلسلة البرمجيات الوسيطة ملاحظة: أضفنا وحدة دليلنا في المسار '/catalog' الذي سيكون البادئة لجميع المسارات المحدَّدة في وحدة الدليل، فمثلًا سيكون عنوان URL هو /catalog/books/ للوصول إلى قائمة الكتب. يجب أن يكون لدينا الآن مسارات ودوال هيكلية مُفعَّلة لجميع عناوين URL التي سندعمها في موقع المكتبة المحلية LocalLibrary. اختبار الوجهات أولًا، ابدأ تشغيل موقع الويب بالطريقة المعتادة، فالطريقة الافتراضية في أنظمة التشغيل هي: // نظام ويندوز SET DEBUG=express-locallibrary-tutorial:* & npm start // ماك أو لينكس DEBUG=express-locallibrary-tutorial:* npm start ولكن إذا سبق لك إعداد nodemon، فيمكنك بدلًا من ذلك استخدام ما يلي: npm run serverstart انتقل بعد ذلك إلى عدد من عناوين URL الخاصة بموقع المكتبة المحلية LocalLibrary وتحقق من عدم ظهور صفحة خطأ (HTTP 404). إليك مجموعة صغيرة من عناوين URL: http://localhost:3000/ http://localhost:3000/catalog http://localhost:3000/catalog/books http://localhost:3000/catalog/bookinstances/ http://localhost:3000/catalog/authors/ http://localhost:3000/catalog/genres/ http://localhost:3000/catalog/book/5846437593935e2f8c2aa226 http://localhost:3000/catalog/book/create الخلاصة أنشأنا جميع الوجهات الخاصة بموقعنا، مع دوال المتحكمات الوهمية التي يمكننا ملؤها بتقديم كامل في مقالات لاحقة، وتعلمنا الكثير من المعلومات الأساسية حول وجهات Express والتعامل مع الاستثناءات وبعض الأساليب لبناء الوجهات والمتحكمات. سننشئ في المقال التالي صفحة ترحيب مناسبة للموقع باستخدام العروض (القوالب) والمعلومات المخزنة في نماذجنا، وسننشئ استمارات HTML وشيفرة معالجة الاستمارات لبدء تعديل البيانات التي يخزنها الموقع. ترجمة -وبتصرُّف- للمقال Express Tutorial Part 4: Routes and controllers. اقرأ أيضًا المقال السابق تطبيق عملي لتعلم Express - الجزء الثاني: استخدام قاعدة البيانات باستخدام مكتبة Mongoose. إنشاء خادم ويب في Node.js باستخدام الوحدة HTTP. المتحكّمات Controllers والعروض Views في إطار العمل Rails.
-
يقدّم هذا المقال مقدمة موجزة عن قواعد البيانات وكيفية استخدامها مع تطبيقات Node/Express، ثم يوضّح كيفية استخدام مكتبة Mongoose لتوفير الوصول إلى قاعدة بيانات موقع المكتبة المحلية LocalLibrary، ويشرح كيفية التصريح عن مخطط الكائنات object schema والنماذج Models، وأنواع الحقول الرئيسية والتحقق الأساسي من صحة البيانات. يعرض أيضًا بإيجاز بعض الطرق الرئيسية التي يمكنك من خلالها الوصول إلى بيانات النموذج. المتطلبات الأساسية: الاطلاع على مقال إنشاء موقع ويب هيكلي لمكتبة محلية. الهدف: أن تكون قادرًا على تصميم وإنشاء نماذجك باستخدام مكتبة Mongoose. يستخدم موظفو المكتبة موقع المكتبة المحلية لتخزين المعلومات حول الكتب والمستعيرين، بينما يستخدمه أعضاء المكتبة لتصفح الكتب والبحث عنها ولمعرفة ما إذا كان هناك أيّ نسخ متاحة ثم حجزها أو استعارتها. لذا سنخزّن المعلومات في قاعدة بيانات لتخزينها واسترجاعها بكفاءة. يمكن لتطبيقات Express استخدام العديد من قواعد البيانات المختلفة، وهناك العديد من الأساليب التي يمكنك استخدامها لإجراء عمليات الإنشاء والقراءة والتحديث والحذف -أو CRUD اختصارًا. يقدم هذا المقال نظرةً عامةً موجزة عن بعض الخيارات المتاحة ثم ينتقل ليشرح الآليات المختارة بالتفصيل. قواعد البيانات الممكن استخدامها يمكن لتطبيقات Express استخدام أيّ قاعدة بيانات تدعمها بيئة Node، إذ لا يحدد إطارعمل Express أيّ سلوك أو متطلبات إضافية محددة لإدارة قاعدة البيانات، وهناك العديد من الخيارات الشائعة بما في ذلك قواعد بيانات PostgreSQL و MySQL و Redis و SQLite و MongoDB وغير ذلك. يجب أن تأخذ في حساباتك عند اختيار قاعدة بيانات أشياءً مثل منحنى الوقت لتعلمها أو للحصول على نتائج والأداء وسهولة التكرار أو النسخ الاحتياطي والتكلفة ودعم المجتمع وغير ذلك. لا توجد قاعدة بيانات أفضل من الأخرى، ولكن تُعَد أيّ قاعدة بيانات شائعة تقريبًا مقبولة بصورة جيدة لموقع صغير إلى متوسط الحجم مثل موقع مكتبتنا المحلية. اطلع على تكامل قاعدة البيانات في توثيق Express لمزيد من المعلومات حول هذه الخيارات. أفضل طريقة للتفاعل مع قاعدة البيانات هناك طريقتان شائعتان للتفاعل مع قاعدة البيانات، هما: استخدام لغة الاستعلام الأصيلة لقواعد البيانات مثل لغة SQL. استخدام نموذج بيانات الكائن Object Data Model -أو اختصارًا ODM- أو نموذج الكائنات العلاقي Object Relational Model -أو ORM اختصارًا. يمثل نموذج ODM/ORM بيانات موقع الويب بوصفها كائنات جافا سكريبت، والتي تُربَط بعد ذلك بقاعدة البيانات الأساسية، إذ ترتبط بعض نماذج ORM بقاعدة بيانات معينة، بينما يوفر بعضها الآخر واجهة خلفية لا تعتمد على قاعدة البيانات. يمكن الحصول على أفضل أداء باستخدام لغة SQL أو أيّ لغة استعلام تدعمها قاعدة البيانات، إذ يُعَد نموذج ODM أبطأ لأنه يستخدم شيفرة ترجمة للربط بين الكائنات وتنسيق قاعدة البيانات، إذ يمكن ألّا يستخدم هذا النموذج استعلامات قاعدة البيانات الأكثر كفاءة، وهذا صحيح بصورة خاصة إذا كان نموذج ODM يدعم واجهات خلفية لقواعد بيانات مختلفة، وبالتالي يجب تقديم تنازلات أكبر فيما يتعلق بميزات قاعدة البيانات المدعومة. تتمثل فائدة استخدام نموذج ORM في أنه يمكن للمبرمجين الاستمرار في التفكير وفق مصطلحات كائنات جافاسكربت بدلًا من دلالات قواعد البيانات، وهذا صحيح بصورة خاصة إذا كنت بحاجة إلى العمل مع قواعد بيانات مختلفة على موقع الويب نفسه أو مواقع ويب مختلفة، ويوفر هذا النموذج أيضًا مكانًا واضحًا لإجراء التحقق من صحة البيانات. ملاحظة: يؤدي استخدام نماذج ODM/ORM إلى انخفاض تكاليف التطوير والصيانة، إذ يجب أن تفكر كثيرًا في استخدام نموذج ODM إلّا إذا كنت على دراية بلغة الاستعلام الأصيلة أو إذا كان الأداء أمرًا بالغ الأهمية. نموذج ORM/ODM الذي يجب أن استخدامه هناك العديد من حلول ODM/ORM المتاحة على موقع مدير الحزم npm (اطلع على odm و orm للتعرف على بعض من هذه الحلول). إليك بعضًا من هذه الحلول الشائعة: Mongoose: هي أداة نمذجة كائنات قاعدة بيانات MongoDB المُصمَّمة للعمل في بيئة غير متزامنة. Waterline: هو نموذج ORM المُستخرَج من إطار عمل الويب Sails القائم على إطار عمل Express. يوفر واجهة برمجة تطبيقات مُوحَّدة للوصول إلى العديد من قواعد البيانات المختلفة، بما في ذلك Redis و MySQL و LDAP و MongoDB و Postgres. Bookshelf: يتميز بواجهات رد النداء Callback التقليدية القائمة على الوعود Promise، مما يوفر دعمًا لمعامَلات قاعدة البيانات وتحميل العلاقات النشط أو النشط المتداخل والارتباطات متعددة الأشكال ودعم علاقات واحد إلى واحد one-to-one وواحد إلى متعدد one-to-many ومتعدد إلى متعدد many-to-many، ويعمل مع قواعد بيانات PostgreSQL و MySQL و SQLite3. يمكنك الاطلاع على مقال العلاقات بين الجداول في SQL على أكاديمية حسوب لمزيدٍ من المعلومات حول العلاقات بين الجداول. Objection: يسهّل قدر الإمكان استخدام قوة لغة SQL الكاملة ومحرك قاعدة البيانات الأساسي، ويدعم SQLite3 و Postgres و MySQL. Sequelize: هو نموذج ORM مبني على الوعود لكلٍّ من Node.js و io.js، ويدعم الأنواع المختلفة من لغات PostgreSQL و MySQL و MariaDB و SQLite و MSSQL ويتميز بدعمٍ قوي للمعامَلات transaction والعلاقات وتكرار عمليات القراءة وغير ذلك. Node ORM2: هو مدير علاقات الكائنات الخاص ببيئة NodeJS، ويدعم MySQL و SQLite و Progress، مما يساعد على العمل مع قاعدة البيانات باستخدام أسلوب موجَّه بالكائنات. GraphQL: لغة استعلام أساسية لواجهات برمجة التطبيقات restful API، وتحظى لغة GraphQL بشعبية كبيرة ولديها ميزات متاحة لقراءة البيانات من قواعد البيانات. يجب مراعاة كل من الميزات المتوفرة ونشاط المجتمع (التنزيلات والمساهمات وتقارير الأخطاء وجودة التوثيق وغير ذلك) عند اختيار الحل المناسب، وتُعَد مكتبة Mongoose أكثر نماذج ODM شيوعًا، وهو خيار جيد عند استخدام MongoDB لقاعدة بياناتك. استخدام مكتبة Mongoose وقاعدة بيانات MongoDB لموقع المكتبة المحلية سنستخدم في مثالنا مكتبة Mongoose بوصفها نموذج ODM للوصول إلى بيانات مكتبتنا، إذ تتصرف هذه المكتبة بمثابة واجهة أمامية لقاعدة بيانات MongoDB، وهي قاعدة بيانات NoSQL مفتوحة المصدر تستخدم نموذج بيانات موجَّهًا بالمستندات، إذ تشبه مجموعة المستندات في قاعدة بيانات MongoDB جدولًا من الصفوف في قاعدة بيانات علاقية. تحظى هذه المجموعة من نموذج ODM وقاعدة البيانات بشعبية كبيرة في مجتمع Node، ويرجع ذلك جزئيًا إلى أن تخزين المستندات ونظام الاستعلام يشبه إلى حد كبير JSON، وبالتالي فهو مألوف لمطوري جافا سكريبت. ملاحظة: لست بحاجة إلى معرفة قاعدة بيانات MongoDB لاستخدام مكتبة Mongoose، بالرغم من أن أجزاءً من توثيق Mongoose أسهل في الاستخدام والفهم إذا كنت على دراية بقاعدة بيانات MongoDB. سنوضح فيما يلي كيفية تعريف والوصول إلى مخطط ونماذج Mongoose لمثال موقع ويب المكتبة المحلية LocalLibrary. تصميم نماذج موقع المكتبة المحلية يفضَّل أخذ بضع دقائق للتفكير في البيانات التي يجب تخزينها والعلاقات بين الكائنات قبل البدء في كتابة شيفرة النماذج، إذ نعلم أننا بحاجةٍ إلى تخزين معلومات حول الكتب (العنوان والملخص والمؤلف ونوع الكتاب ورقم ISBN)، وقد يكون لدينا نسخٌ متعددة متاحة (مع معرّفات فريدة عامة وحالات توفرها وغير ذلك)، ويمكن أن نحتاج إلى تخزين مزيدٍ من المعلومات حول المؤلف أكثر من مجرد اسمه، ويمكن أن يكون هناك عدة مؤلفين لهم الاسم نفسه أو أسماء متشابهة. نريد أن نكون قادرين على فرز المعلومات بناءً على عنوان الكتاب والمؤلف ونوع الكتاب Genre وفئته Category. من المنطقي أن يكون لديك نماذج منفصلة لكل كائن (مجموعة من المعلومات المتعلقة ببعضها) عند تصميم نماذجك، وبالتالي فإن بعض المرشّحين الواضحين لهذه النماذج هم الكتب ونسخ الكتب والمؤلفون. يمكن أن ترغب في استخدام النماذج لتمثيل خيارات قائمة الاختيار (مثل قائمة اختيارات منسدلة) بدلًا من كتابة شيفرة ثابتة للخيارات في موقع الويب نفسه، إذ يوصَى بذلك عندما لا تكون جميع الخيارات معروفة مسبقًا أو أنها ستتغير، ومن الأمثلة الجيدة لذلك هو نوع الكتاب، مثل النوع الخيالي والخيال العلمي وغير ذلك. يجب الآن التفكير في العلاقات بين النماذج والحقول بعد تحديدها، إذ يوضح مخطط ارتباط باستخدام لغة UML الآتي النماذج التي سنعرّفها في حالتنا (على شكل صناديق)، إذ أنشأنا نماذجًا للكتاب (تفاصيل الكتاب العامة)، ونسخة الكتاب (حالة نسخ الكتاب الحقيقية المحدَّدة المتاحة في النظام)، والمؤلف، وقررنا أيضًا أن يكون لدينا نموذج لنوع الكتاب بحيث يمكن إنشاء القيم ديناميكيًا. لم ننشئ نموذجًا لحالة نسخة الكتاب BookInstance:status، إذ سنجعل القيم المقبولة ثابتة لأننا لا نتوقع تغييرها. يمكنك رؤية اسم النموذج وأسماء الحقول وأنواعها والتوابع وأنواع الإعادة الخاصة بها في كل صندوق. يوضح المخطط البياني الآتي أيضًا العلاقات بين النماذج، بما في ذلك درجة تعدّدها Multiplicities، وهي الأعداد الموجودة على المخطط والتي توضح عدد أو الحد الأقصى والحد الأدنى لكل نموذج الذي يمكن أن يكون موجودًا في العلاقة، فمثلًا يوضّح الخط المتصل بين الصناديق أن الكتاب Book والنوع Genre مرتبطان، وتوضح الأعداد القريبة من نموذج الكتاب Book أنه يجب يكون للنوع Genre صفر أو أكثر من الكتب Book (بقدر ما تريد)، بينما توضح الأعداد الموجودة على الطرف الآخر من الخط بجوار نموذج النوع Genre أن الكتاب يمكن أن يكون له صفر أو أكثر من الأنواع Genre المتعلقة به. ملاحظة: يُفضَّل غالبًا أن يكون لديك الحقل الذي يعرّف العلاقة بين المستندات/النماذج في نموذج واحد فقط كما سنوضّح لاحقًا، ولا يزال بإمكانك العثور على العلاقة العكسية من خلال البحث عن _id المرتبط بها في النموذج الآخر. اخترنا فيما يلي تعريف العلاقة بينBook/Genre و Book/Author في مخطط Schema الكتاب Book، والعلاقة بين Book/BookInstance في مخطط نسخة الكتاب BookInstance، إذ كان هذا الاختيار عشوائيًا إلى حدٍ ما، وكان من الممكن أيضًا أن يكون أحد الحقول موجودًا في المخطط الآخر. ملاحظة: يوفر القسم التالي شرحًا بسيطًا عن كيفية تعريف النماذج واستخدامها، لذلك ضع في بالك أثناء القراءة كيف سنبني كل نموذج من النماذج الموجودة في المخطط البياني السابق. واجهات برمجة تطبيقات قاعدة البيانات غير المتزامنة تُعَد توابع قاعدة البيانات لإنشاء السجلات أو العثور عليها أو تحديثها أو حذفها غير متزامنة، وهذا يعني أن التوابع تعيد القيم مباشرةً ويكون تشغيل الشيفرة البرمجية الخاصة بمعالجة نجاح أو فشل التابع في وقت لاحق عند اكتمال العملية. يمكن تنفيذ شيفرة برمجية أخرى أثناء انتظار الخادم لاكتمال عملية قاعدة البيانات، لذلك يمكن أن يظل الخادم مستجيبًا للطلبات الأخرى. تحتوي لغة جافا سكريبت Javascript على عدد من الآليات لدعم السلوك غير المتزامن، إذ اعتمدت كثيرًا سابقًا على تمرير دوال رد النداء إلى توابع غير متزامنة لمعالجة حالات النجاح والخطأ، وحلّت الوعود Promises محل دوال رد النداء إلى حد كبير في لغة جافا سكربت الحديثة. الوعود هي كائنات يعيدها (مباشرةً) تابع غير متزامن يمثل حالتها المستقبلية، ويستقر كائن الوعد عند اكتمال العملية، ويحقق كائنًا يمثل نتيجة العملية أو الخطأ. هناك طريقتان رئيسيتان يمكنك من خلالهما استخدام الوعود لتشغيل الشيفرة البرمجية عند استقرار الوعد، إذ نوصي بقراءة كيفية استخدام الوعود للحصول على نظرة عامة عالية المستوى على كلا الأسلوبين. سنستخدم في هذا المقال await لانتظار اكتمال الوعد في async function، لأن هذا الأسلوب يؤدي إلى الحصول على شيفرة برمجية غير متزامنة مفهومة وأكثر قابلية للقراءة. تتمثل الطريقة التي يعمل بها هذا الأسلوب في أنك تستخدم الكلمة الأساسية async function لتمييز الدالة بوصفها غير متزامنة، ثم تطبّق ضمن هذه الدالة تابع await على أيّ تابع يعيد وعدًا، وتتوقف عملية الدالة غير المتزامنة عند تنفيذها مؤقتًا عند أول تابع await حتى استقرار الوعد، ثم تعيد الدالة غير المتزامنة ويمكنك تشغيل الشيفرة البرمجية الموجودة بعد تلك الدالة من منظور الشيفرة البرمجية المحيطة. يعيد التابع await ضمن الدالة غير المتزامنة النتيجة لاحقًا عند استقرار الوعد، أو يعطي خطأً عند رفض الوعد، ثم تُنفَّذ الشيفرة البرمجية الموجودة في الدالة غير المتزامنة حتى يظهر تابع await آخر، إذ ستتوقف مؤقتًا مرةً أخرى، أو حتى تشغيل الشيفرة البرمجية بأكملها الموجودة في الدالة. يمكنك أن ترى كيفية عمل هذه الطريقة في المثال الآتي، إذ تُعَد myFunction() دالة غير متزامنة تُستدعَى ضمن كتلة try...catch. يُوقََف تنفيذ الشيفرة البرمجية مؤقتًا في التابع methodThatReturnsPromise() عند تشغيل الدالة myFunction() حتى تحقيق الوعد، وعندها يستمر تنفيذ الشيفرة البرمجية حتى الوصول إلى التابع aFunctionThatReturnsPromise() وينتظر مرةً أخرى. تُشغَّل الشيفرة البرمجية الموجودة في كتلة catch عند رمي خطأ في الدالة غير المتزامنة، إذ سيحدث ذلك عند رفض الوعد الذي يعيده أيّ من هذين التابعين. async function myFunction { // ... await someObject.methodThatReturnsPromise(); // ... await aFunctionThatReturnsPromise(); // ... } try { // ... myFunction(); // ... } catch (e) { // شيفرة معالجة الخطأ } تُشغَّل التوابع غير المتزامنة السابقة تسلسليًا، وإذا لم تعتمد التوابع على بعضها بعضًا، فيمكنك تشغيلها على التوازي وإنهاء العملية بأكملها بسرعة أكبر، ويمكن تحقيق ذلك باستخدام التابع Promise.all() الذي يأخذ تكرارًا من الوعود بوصفها دخلًا ويعيد وعدًا Promise واحدًا. يمكن الوفاء بهذا الوعد المُعاد عند الوفاء بجميع وعود الدخل مع مجموعة من قيم الوفاء، ويرفَض عند رفض أيٍّ من وعود الدخل مع سبب الرفض الأول. توضح الشيفرة البرمجية التالية كيفية عمل ذلك، إذ لدينا أولًا دالتان تعيدان وعودًا، إذ ننتظرهما await حتى يكتملا باستخدام الوعد الذي يعيده التابع Promise.all(). يعيد await بمجرد أن تكتمل كلتا الدالتين وتُملَأ مصفوفة النتائج، ثم يستمر تنفيذ الدالة حتى الوصول إلى تابع await التالي، وتنتظر حتى استقرار الوعد الذي تعيده الدالة anotherFunctionThatReturnsPromise(). يمكنك استدعاء الدالة myFunction() في كتلة try...catch لالتقاط الأخطاء. async function myFunction { // ... const [resultFunction1, resultFunction2] = await Promise.all([ functionThatReturnsPromise1(), functionThatReturnsPromise2() ]); // ... await anotherFunctionThatReturnsPromise(resultFunction1); } تسمح الوعود مع await/async بالتحكم المرن والمنطقي بالتنفيذ غير المتزامن. مقدمة إلى مكتبة Mongoose يقدم هذا القسم نظرةً عامة حول كيفية توصيل مكتبة Mongoose بقاعدة بيانات MongoDB، وكيفية تعريف المخططات والنماذج، وكيفية تطبيق الاستعلامات الأساسية. تثبيت Mongoose و MongoDB تُثبَّت مكتبة Mongoose في مشروعك (في الملف package.json) مثل أي اعتمادية أخرى باستخدام مدير حزم npm، إذ يمكن تثبيتها باستخدم الأمر التالي في مجلد مشروعك: npm install mongoose يضيف تثبيت مكتبة Mongoose جميع اعتمادياتها بما في ذلك مشغِّل قاعدة بيانات MongoDB، لكنه لا يؤدي إلى تثبيت MongoDB. إذا أدرتَ تثبيت خادم MongoDB، فيمكنك تنزيل المثبِّتات لأنظمة تشغيل مختلفة وتثبيتها محليًا، ويمكنك استخدام نسخ من MongoDB المستندة إلى السحابة. ملاحظة: سنستخدم في هذا المقال قاعدة البيانات المستندة إلى السحابة MongoDB Atlas بوصفها طبقة خدمة مجانية لتوفير قاعدة البيانات، وهي مناسبة لبيئة التطوير والتعلم لأنها تجعل نظام تشغيل التثبيت مستقلًا، وتُعَد قاعدة البيانات التي تمثل خدمة Database-as-a-service أيضًا إحدى الطرق التي يمكن أن تستخدمها لقاعدة بيانات بيئة الإنتاج. الاتصال بقاعدة بيانات MongoDB تتطلب مكتبة Mongoose اتصالًا بقاعدة بيانات MongoDB، وذلك باستخدام الدالة require() والاتصال بقاعدة بيانات مستضافة محليًا باستخدام mongoose.connect() كما يلي، ولكن سنتّصل بدلًا من ذلك في هذا المقال بقاعدة بيانات مستضافة عبر الإنترنت: // استيراد وحدة mongoose const mongoose = require("mongoose"); // اضبط `strictQuery: false` للاشتراك العام في الترشيح وفق الخاصيات غير المُدرَجة في المخطط // لأن هذا الخيار يزيل تحذيرات Mongoose 7 الأولية. // اطّلع على https://mongoosejs.com/docs/migrating_to_6.html#strictquery-is-removed-and-replaced-by-strict mongoose.set("strictQuery", false); // حدّد عنوان URL لقاعدة البيانات للاتصال به const mongoDB = "mongodb://127.0.0.1/my_database"; // انتظر حتى الاتصال بقاعدة البيانات، مع تسجيل خطأ إذا كانت هناك مشكلة main().catch((err) => console.log(err)); async function main() { await mongoose.connect(mongoDB); } ملاحظة: ننتظر await الوعد الذي يعيده التابع connect() ضمن دالة مُصرَّح عنها باستخدام async function كما ناقشنا سابقًا في قسم واجهات برمجة التطبيقات لقاعدة البيانات غير المتزامنة من هذا المقال. نستخدم المعالج catch() الخاص بالوعد لمعالجة الأخطاء عند محاولة الاتصال، ولكن يمكن أيضًا استدعاء main() ضمن كتلة try...catch. يمكنك الحصول على كائن Connection الافتراضي باستخدام mongoose.connection، وإذا كنت بحاجة إلى إنشاء اتصالات إضافية، فيمكنك استخدام التابع mongoose.createConnection() الذي يأخذ صيغة معرّف URI نفسه الخاص بقاعدة البيانات (مع المضيف وقاعدة البيانات والمنفذ والخيارات وإلخ) الذي يستخدمه التابع connect() ويعيد كائن Connection. لاحظ أن createConnection() يعيد مباشرةً، وبالتالي إذا كنت بحاجة إلى الانتظار حتى إنشاء الاتصال، فيمكنك استدعاؤه مع asPromise() لإعادة وعد (mongoose.createConnection(mongoDB).asPromise()). تعريف وإنشاء النماذج تُعرَّف النماذج باستخدام الواجهة Schema التي تتيح تعريف الحقول المُخزَّنة في المستندات مع متطلبات التحقق من صحة البيانات والقيم الافتراضية. يمكنك أيضًا تعريف التوابع المساعدة الثابتة ونُسَخ منها لتسهيل العمل مع أنواع بياناتك، والخاصيات الافتراضية التي يمكنك استخدامها مثل الحقول الأخرى، ولكنها غير مخزنة في قاعدة البيانات فعليًا (سنناقش ذلك لاحقًا). تُصرَّف بعد ذلك واجهات Schema إلى نماذج باستخدام التابع mongoose.model()، ثم يمكنك استخدام النموذج للعثور على كائنات من نوعٍ محدَّد وإنشائها وتحديثها وحذفها. ملاحظة: يُربَط كل نموذج بمجموعة من المستندات في قاعدة بيانات MongoDB، إذ ستحتوي المستندات على أنواع الحقول/المخططات Schema المحددة في نموذج Schema. تعريف المخططات يوضّح جزء الشيفرة البرمجية التالي كيفية تعريف مخطط بسيط، إذ يجب أولًا طلب مكتبة mongoose باستخدام الدالة require()، ثم استخدم باني Schema لإنشاء نسخة من المخطط الجديد، مع تعريف الحقول المختلفة ضمنه في معامل باني الكائن. // طلب مكتبة Mongoose const mongoose = require("mongoose"); // تعريف مخطط const Schema = mongoose.Schema; const SomeModelSchema = new Schema({ a_string: String, a_date: Date, }); لدينا في المثال السابق حقلين فقط نوعاهما: سلسلة نصية String وتاريخ Date، وسنعرض في الأقسام التالية بعض أنواع الحقول الأخرى والتحقق من صحتها والتوابع الأخرى. إنشاء نموذج تُنشَأ النماذج من المخططات باستخدام التابع mongoose.model() كما يلي: // تعريف مخطط const Schema = mongoose.Schema; const SomeModelSchema = new Schema({ a_string: String, a_date: Date, }); // تصريف النموذج من المخطط const SomeModel = mongoose.model("SomeModel", SomeModelSchema); الوسيط الأول هو الاسم المفرد للمجموعة التي ستُنشَأ لنموذجك، إذ ستنشِئ مكتبة Mongoose مجموعة قاعدة البيانات للنموذج SomeModel السابق، والوسيط الثاني هو المخطط الذي تريد استخدامه في إنشاء النموذج. ملاحظة: يمكنك استخدام أصناف Classes نموذجك بعد تعريفها لإنشاء سجلات أو تحديثها أو حذفها، ويمكنك تشغيل الاستعلامات للحصول على جميع السجلات أو مجموعات فرعية معينة من السجلات. سنوضح كيفية تحقيق ذلك لاحقًا في قسم استخدام النماذج وعندما ننشئ العروض Views. أنواع المخططات والحقول يمكن أن يحتوي المخطط على عدد عشوائي من الحقول، إذ يمثل كلٌّ منها حقلًا في المستندات المخزنة في قاعدة بيانات MongoDB. يوضّح المثال التالي مخططًا يحتوي على العديد من أنواع الحقول وكيفية التصريح عنها: const schema = new Schema({ name: String, binary: Buffer, living: Boolean, updated: { type: Date, default: Date.now() }, age: { type: Number, min: 18, max: 65, required: true }, mixed: Schema.Types.Mixed, _someId: Schema.Types.ObjectId, array: [], ofString: [String], // يمكنك أيضًا الحصول على مصفوفة لكل نوع من الأنواع الأخرى nested: { stuff: { type: String, lowercase: true, trim: true } }, }); لا تحتاج معظم أنواع المخططات SchemaTypes (الواصفات الموجودة بعد "type:" أو بعد أسماء الحقول) شرحًا، ولكن هناك بعض الاستثناءات وهي: ObjectId: يمثل نسخًا محدّدة لنموذجٍ في قاعدة البيانات، فمثلًا يمكن أن يستخدم الكتاب هذا النوع لتمثيل كائن مؤلفه، وسيحتوي هذا النوع على معرّف فريد (_id) للكائن، ويمكننا استخدام التابع populate() لسحب المعلومات عند الحاجة. Mixed: نوع مخطط عشوائي. []: مصفوفة من العناصر، إذ يمكنك إجراء عمليات مصفوفات جافاسكربت على هذه النماذج (الدفع والسحب إلغاء الإزاحة وإلخ). توضح الأمثلة السابقة مصفوفةً من الكائنات بدون نوع محدد ومصفوفة من كائنات String، ولكن يمكن أن يكون لديك مصفوفة من أيّ نوع من الكائنات. توضّح الشيفرة البرمجية أيضًا طريقتين للتصريح عن الحقل هما: اسم الحقل ونوعه مثل زوج قيمة-مفتاح (كما هو الحال مع اسم الحقول name و binary و living مثلًا). اسم الحقل متبوعًا بكائن يحدد النوع type وأيّ خيارات أخرى للحقل، إذ تتضمن هذه الخيارات ما يلي: قيم افتراضية. أدوات التحقق المبنية مسبقًا، مثل القيم العليا أو الدنيا، ودوال التحقق من صحة البيانات المُخصَّصة. ما إذا كان الحقل مطلوبًا. ما إذا كان يجب ضبط حقول String تلقائيًا بأحرف صغيرة أو كبيرة أو حذف المسافات في بداية ونهاية السلسلة النصية، مثل: { type: String, lowercase: true, trim: true } اطّلع على أنواع المخططات في توثيق Mongoose لمزيد من المعلومات حول الخيارات. التحقق من صحة البيانات توفر مكتبة Mongoose أدوات تحقق من صحة البيانات مبنية مسبقًا ومخصصة، وأدوات تحقق متزامنة وغير متزامنة، وتسمح بتحديد كلٍّ من مجال القيم المقبول ورسالة الخطأ التي تمثل فشل التحقق من صحة البيانات في جميع الحالات. تتضمن أدوات التحقق من صحة البيانات المبنية مسبقًا ما يلي: تحتوي جميع أنواع المخططات على أداة التحقق required المبنية مسبقًا التي تُستخدم لتحديد ما إذا كان يجب توفير الحقل لحفظ مستندٍ ما. تحتوي الأعداد Numbers على أدوات تحقق من صحة الحد الأدنى min والحد الأعلى max. تحتوي السلاسل النصية Strings على أدوات التحقق التالية: enum: تحدد مجموعة القيم المسموح بها للحقل. match: تحدد التعبير النمطي Regular Expression الذي يجب أن تتطابق معه السلسلة النصية. الطول الأقصى maxLength والطول الأدنى minLength للسلسلة النصية. يوضح المثال التالي -المأخوذ من توثيق Mongoose- كيفية تحديد بعض أنواع أدوات التحقق من صحة البيانات ورسائل الخطأ: const breakfastSchema = new Schema({ eggs: { type: Number, min: [6, "Too few eggs"], max: 12, required: [true, "Why no eggs?"], }, drink: { type: String, enum: ["Coffee", "Tea", "Water"], }, }); اطّلع على التحقق من صحة البيانات في توثيق Mongoose للحصول على معلومات كاملة حول التحقق من صحة الحقول. الخاصيات الافتراضية الخاصيات الافتراضية هي خاصيات المستند التي يمكنك جلبها وضبطها دون استمرار وجودها في قاعدة بيانات MongoDB، إذ تُعَد الجوالب Getters مفيدة لتنسيق الحقول أو دمجها، وتكون الضوابط Setters مفيدة في تفكيك قيمة واحدة إلى قيم متعددة لتخزينها. يبني المثال الموجود في توثيق Mongoose (ويهدم) خاصية افتراضية للاسم الكامل من حقل الاسم الأول والأخير، ويُعَد ذلك أسهل وأنظف من بناء اسم كامل في كل مرة يُستخدَم أحدها في قالب. ملاحظة: سنستخدم خاصية افتراضية في موقع المكتبة المحلية لتعريف عنوان URL فريد لكل سجل نموذج باستخدام مسار وقيمة _id الخاصة بالسجل. اطلع على الخاصيات الافتراضية في توثيق Mongoose لمزيد من المعلومات. التوابع والاستعلامات المساعدة يمكن أن يحتوي المخطط أيضًا على نسخ من التوابع Instance methods وتوابع ثابتة static methods واستعلامات مساعدة query helpers، إذ تتشابه نسخ التوابع والتوابع الثابتة، ولكن مع وجود اختلاف واضح في أن نسخ التوابع مرتبطة بسجل معين ويمكنها الوصول إلى الكائن الحالي. تسمح الاستعلامات المساعدة بتوسيع واجهة برمجة تطبيقات باني الاستعلامات القابلة للتسلسل الخاصة بمكتبة mongoose، مثل السماح بإضافة استعلام وفق الاسم "byName"، إضافةً إلى توابع find() و findOne() و findById(). استخدام النماذج يمكنك بعد إنشاء مخطط استخدامه لإنشاء النماذج، إذ يمثل النموذج مجموعة من المستندات الموجودة في قاعدة البيانات التي يمكنك البحث عنها، بينما تمثل نسخ النموذج المستندات الفردية التي يمكنك حفظها واسترجاعها. سنقدم فيما يلي نظرة عامة موجزة، لذا يمكنك الاطلاع على النماذج في توثيق Mongoose لمزيد من المعلومات. ملاحظة: يُعَد إنشاء السجلات وتحديثها وحذفها والاستعلام عنها عمليات غير متزامنة تعيد وعدًا. سنوضح في الأمثلة التالية استخدام التوابع المتعلقة بهذا الموضوع والتابع await، أي سنوضح الشيفرة البرمجية الأساسي لاستخدام التوابع، إذ سنحذف دالة async function المحيطة وكتلة try...catch لالتقاط الأخطاء للتوضيح. إنشاء وتعديل المستندات يمكنك إنشاء سجل من خلال تعريف نسخة من النموذج ثم استدعاء save(). تفترض الأمثلة التالية أن SomeModel هو نموذج (له حقل واحد هو name) أنشأناه من المخطط. // أنشئ نسخة من النموذج SomeModel const awesome_instance = new SomeModel({ name: "awesome" }); // احفظ نسخة النموذج الجديدة بطريقة غير متزامنة await awesome_instance.save(); يمكنك أيضًا استخدام create() لتعريف نسخة من النموذج في الوقت الذي تحفظها فيه. سننشئ فيما يلي نسخة واحدة فقط، ولكن يمكنك إنشاء نسخ متعددة من خلال تمرير مصفوفة من الكائنات. await SomeModel.create({ name: "also_awesome" }); لكل نموذج اتصاله المرتبط به، والذي سيكون الاتصال الافتراضي عند استخدام mongoose.model()، ويمكنك إنشاء اتصال جديد واستدعاء .model() لإنشاء المستندات في قاعدة بيانات مختلفة. يمكنك الوصول إلى الحقول في هذا السجل الجديد باستخدام الصيغة النقطية وتغيير القيم، ويجب استدعاء save() أو update() لتخزين القيم المُعدَّلة في قاعدة البيانات. // الوصول إلى قيم حقول النموذج باستخدام الصيغة النقطية console.log(awesome_instance.name); // يجب تسجيل 'also_awesome' أيضًا // تغيير السجل من خلال تعديل الحقول ثم استدعاء save() awesome_instance.name = "New cool name"; await awesome_instance.save(); البحث عن السجلات يمكنك البحث عن السجلات باستخدام توابع الاستعلام من خلال تحديد شروط الاستعلام بوصفها مستند JSON. يوضح جزء الشيفرة التالي كيفية العثور على جميع الرياضيين Athlete الذين يلعبون كرة المضرب في قاعدة بيانات، ويعيد فقط حقول اسم name وعُمر age الرياضي، إذ نحدد فقط حقلًا واحدًا مطابقًا (الرياضة sport)، ولكن يمكنك إضافة مزيدٍ من المعايير أو تحديد معايير التعبير النمطي أو إزالة جميع الشروط لإعادة جميع الرياضيين. const Athlete = mongoose.model("Athlete", yourSchema); // العثور على جميع الرياضيين الذين يلعبون كرة المضرب مع تحديد حقول 'name' و 'age' const tennisPlayers = await Athlete.find( { sport: "Tennis" }, "name age" ).exec(); ملاحظة: من المهم أن تتذكر أن عدم العثور على أي نتائج ليس خطأً في البحث، ولكنه يمكن أن يكون حالة فشل في سياق تطبيقك. إذا توقّع تطبيقك بحثًا ما للعثور على قيمة، فيمكنك التحقق من عدد المدخلات المُعَادة في النتيجة. تعيد واجهات برمجة تطبيقات الاستعلام مثل find() متغيرًا من النوع Query، ويمكنك استخدام كائن استعلام لبناء استعلام ضمن أجزاء قبل تنفيذه باستخدام التابع exec() الذي ينفّذ الاستعلام ويعيد وعدًا يمكنك انتظاره باستخدام await للحصول على النتيجة. // العثور على جميع الرياضيين الذين يلعبون كرة المضرب const query = Athlete.find({ sport: "Tennis" }); // اختيار حقول 'name' و 'age' query.select("name age"); // قصر نتائجنا على 5 عناصر query.limit(5); // الفرز وفق العمر query.sort({ age: -1 }); // تنفيذ الاستعلام في وقت لاحق query.exec(); عرّفنا شروط الاستعلام في التابع find()، ويمكننا تطبيق ذلك أيضًا باستخدام الدالة where()، ويمكننا سَلسَلة جميع أجزاء الاستعلام مع بعضها باستخدام المعامل النقطي (.) بدلًا من إضافتها بصورة منفصلة. جزء الشيفرة البرمجية التالي هو الاستعلام السابق نفسه مع شرط إضافي للعُمر: Athlete.find() .where("sport") .equals("Tennis") .where("age") .gt(17) .lt(50) // استعلام where إضافي .limit(5) .sort({ age: -1 }) .select("name age") .exec(); يحصل التابع find() على جميع السجلات المطابقة، ولكنك تريد الحصول على تطابق واحد فقط في أغلب الأحيان، لذا يمكنك استخدام توابع الاستعلام التالية لسجل واحد: findById(): يبحث عن المستند باستخدام المعرّف id، فلكل مستندٍ معرّفٌ فريد. findOne(): يبحث عن مستند واحد يطابق معاييرًا محدَّدة. findByIdAndRemove() و findByIdAndUpdate() و findOneAndRemove() و findOneAndUpdate(): تبحث عن مستند واحد باستخدام المعرّف id أو المعايير، فإما أن تحدّثه أو تزيله، إذ تُعَد هذه الدوال ملائمة ومفيدة لتحديث السجلات وإزالتها. ملاحظة: هناك أيضًا التابع countDocuments() الذي يمكنك استخدامه للحصول على عدد العناصر التي تطابق الشروط، ويُعَد مفيدًا إذا أردتَ إجراء تعداد دون جلب السجلات فعليًا. هناك الكثير من الأمور التي يمكنك تطبيقها على الاستعلامات، لذا اطّلع على الاستعلامات في توثيق Mongoose لمزيد من المعلومات. العمل مع المستندات- الملء Population يمكنك إنشاء مراجعٍ من نسخة مستند أو نموذج إلى آخر باستخدام حقل المخطط ObjectId، أو من مستند إلى عدة مستندات باستخدام مصفوفة من ObjectId. يخزّن هذا الحقل معرّف النموذج المرتبط به، وإذا كنت بحاجة إلى محتوى المستند الفعلي، فيمكنك استخدام التابع populate() في استعلام لاستبدال المعرّف بالبيانات الفعلية. يعرّف المخطط التالي مثلًا المؤلفين والقصص، إذ يمكن أن يكون لكل مؤلف قصص متعددة، والتي نمثلها بوصفها مصفوفة من ObjectId، ويمكن أن يكون لكل قصة مؤلف واحد. تخبر الخاصية ref المخطط بالنموذج الذي يمكن إسناده إلى هذا الحقل. const mongoose = require("mongoose"); const Schema = mongoose.Schema; const authorSchema = Schema({ name: String, stories: [{ type: Schema.Types.ObjectId, ref: "Story" }], }); const storySchema = Schema({ author: { type: Schema.Types.ObjectId, ref: "Author" }, title: String, }); const Story = mongoose.model("Story", storySchema); const Author = mongoose.model("Author", authorSchema); يمكننا حفظ المراجع التي تشير إلى المستند من خلال إسناد قيمة _id إليها، إذ سننشئ فيما يلي مؤلفًا ثم ننشئ قصة ونسند معرّف المؤلف إلى حقل مؤلف القصة: const bob = new Author({ name: "Bob Smith" }); await bob.save(); // Bob موجود الآن، لذا لننشئ قصة const story = new Story({ title: "Bob goes sledding", author: bob._id, // إسناد _id للمؤلف Bob، إذ يُنشَأ هذا المعرّف افتراضيًا }); await story.save(); ملاحظة: إحدى الفوائد الرائعة لهذا النمط من البرمجة هي أنه لا يتعين علينا تعقيد المسار الرئيسي لشيفرتنا البرمجية من خلال التحقق من الأخطاء، فإذا فشلت أيّ عملية حفظ save()، سيُرفَض الوعد وسيُرمَى خطأ. تتعامل شيفرة معالجة الأخطاء مع ذلك بصورة منفصلة (في كتلة catch() عادةً)، لذا يُعَد الهدف من شيفرتنا البرمجية واضحًا جدًا. يحتوي مستند القصة الآن على مؤلف يُشار إليه باستخدام معرّف مستند المؤلف، ونستخدم التابع populate() كما هو موضح فيما يلي للحصول على معلومات المؤلف في نتائج القصة: Story.findOne({ title: "Bob goes sledding" }) .populate("author") // استبدال معرّف المؤلف بمعلومات المؤلف الفعلية في النتائج .exec(); ملاحظة: سيلاحظ القراء المتمرسون أننا أضفنا مؤلفًا إلى القصة، لكننا لم نفعل أي شيء لإضافة القصة إلى مصفوفة stories الخاصة بالمؤلف. تتمثل إحدى الطرق للحصول على جميع القصص لمؤلف معين في إضافة القصة إلى مصفوفة القصص، ولكن ذلك يمكن أن يؤدي إلى وجود مكانين للاحتفاظ بالمعلومات المتعلقة بالمؤلفين والقصص. توجد طريقة أفضل، وهي الحصول على معرّف _id المؤلف، ثم استخدام find() للبحث عنه في حقل المؤلف عبر جميع القصص. Story.find({ author: bob._id }).exec(); اطلّع على Population في توثيق Mongoose لمزيد من المعلومات التفصيلية. مخطط أو نموذج واحد لكل ملف يمكنك إنشاء مخططات ونماذج باستخدام أي بنية ملفات تريدها، ولكننا نوصي بتعريف كل مخطط نموذج في وحدته أو ملفه الخاص، ثم تصدير التابع لإنشاء النموذج كما يلي: // الملف: ./models/somemodel.js // طلب Mongoose const mongoose = require("mongoose"); // تعريف مخطط const Schema = mongoose.Schema; const SomeModelSchema = new Schema({ a_string: String, a_date: Date, }); // تصدير دالة لإنشاء صنف النموذج "SomeModel" module.exports = mongoose.model("SomeModel", SomeModelSchema); يمكنك بعد ذلك طلب النموذج واستخدامه مباشرةً في ملفات أخرى، وسنوضح فيما يلي كيفية استخدامه للحصول على جميع نسخ النموذج: // إنشاء نموذج SomeModel من خلال طلب الوحدة const SomeModel = require("../models/somemodel"); // استخدام كائن (نموذج) SomeModel للعثور على كافة سجلات SomeModel const modelInstances = await SomeModel.find().exec(); إعداد قاعدة بيانات MongoDB تعرّفنا على ما يمكن أن تفعله مكتبة Mongoose وكيفية تصميم نماذجنا، وحان الوقت الآن لبدء العمل على موقع المكتبة المحلية LocalLibrary، وأول شيء يجب فعله هو إعداد قاعدة بيانات MongoDB التي يمكننا استخدامها لتخزين بيانات مكتبتنا. سنستخدم في هذا المقال قاعدة البيانات التجريبية المُستضافَة على السحابة MongoDB Atlas، إذ لا تُعَد طبقة قاعدة البيانات هذه مناسبة لمواقع الويب في بيئة الإنتاج لأنها لا تحتوي على تكرار Redundancy، ولكنها رائعة لعملية التطوير والنماذج الأولية، وسنستخدمها لأنها مجانية وسهلة الإعداد، ولأنها بائع شائع لقاعدة البيانات التي تمثل خدمة، والتي يمكن أن تختارها لقاعدة بيانات الإنتاج الخاصة بك، وتشمل الخيارات الشائعة الأخرى Compose و ScaleGrid و ObjectRocket. ملاحظة: يمكنك أيضًا إعداد قاعدة بيانات MongoDb محليًا من خلال تنزيل وتثبيت الملفات الثنائية المناسبة لنظامك، إذ ستكون بقية الإرشادات الواردة في هذا المقال متشابهة باستثناء عنوان URL لقاعدة البيانات الذي يمكن أن تحدده عند الاتصال. نستضيف لاحقًا في مقال نشر تطبيق Express في بيئة الإنتاج كلًا من التطبيق وقاعدة البيانات على منصة Railway، ولكن يمكن أيضًا استخدام قاعدة بيانات على MongoDB Atlas. يجب أولًا إنشاء حساب على MongoDB Atlas، وهو مجاني ويتطلب فقط إدخال تفاصيل الاتصال الأساسية والإقرار بشروط الخدمة. ستنتقل بعد تسجيل الدخول إلى الشاشة الرئيسية، لذا اتبع الخطوات التالية: أولًا، انقر على زر "إنشاء Create" في قسم نظرة عامة Overview. ثانيًا، سيؤدي ذلك إلى فتح شاشة نشر قاعدة بيانات سحابية Deploy a cloud database. انقر على زر MO FREE. ثالثًا، سيؤدي ذلك إلى سرد خيارات مختلفة للاختيار بينها: حدد أي مزوّد من قسم المزوّد والمنطقة Provider & Region، إذ تقدّم المناطق المختلفة مزوّدين مختلفين. يمكنك تغيير اسم العنقود ضمن قسم اسم العنقود Cluster Name، إذ سنسميه Cluster0. انقر بعد ذلك على زر "إنشاء عنقود Create Cluster"، وسيستغرق إنشاء العنقود بضع دقائق. رابعًا، سيؤدي ذلك إلى فتح قسم بداية سريعة للأمان Security Quickstart. أدخِل اسم المستخدم وكلمة المرور، وتذكّر نسخ الاعتماديات وتخزينها بأمان إذ سنحتاج إليها لاحقًا. انقر على زر "إنشاء مستخدم Create User". ملاحظة: تجنب استخدام محارف خاصة في كلمة مرور مستخدم MongoDB لأن مكتبة Mongoose يمكن ألّا يحلّل سلسلة الاتصال بصورة صحيحة. أدخل 0.0.0.0/0 في حقل عنوان IP الذي يخبر قاعدة بيانات MongoDB أننا نريد السماح بالوصول من أيّ مكان، ثم انقر على زر "إضافة إدخال Add Entry". ملاحظة: يُعَد قصر عناوين IP التي يمكنها الاتصال على قاعدة بياناتك ومواردك الأخرى من أفضل الممارسات، ولكن سنسمح في مثالنا بالاتصال من أيّ مكان لأننا لا نعرف من أين سيأتي الطلب بعد النشر. انقر بعد ذلك على زر "إنهاء وإغلاق Finish and Close". خامسًا، سيؤدي ذلك إلى فتح الشاشة التالية، لذا انقر على زر "الانتقال إلى قواعد البيانات Go to Databases". سادسًا، ستعود بعد ذلك إلى شاشة نظرة عامة Overview. انقر على قسم قاعدة البيانات Database تحت قائمة "Deployment" الموجودة على اليسار وانقر على زر استعراض التجميعات Browse Collections. سابعًا، سيؤدي ذلك إلى فتح قسم التجميعات Collections. انقر على زر "إضافة بياناتي الخاصة Add My Own Data". ثامنًا، ستظهر الآن شاشة إنشاء قاعدة بيانات Create Database. أدخِل الاسم local_library لاسم قاعدة البيانات الجديدة، ثم أدخِل اسم المجموعة Collection0، ثم انقر على زر "إنشاء Create" لإنشاء قاعدة البيانات. تاسعًا، ستعود إلى شاشة المجموعات Collections مع وجود قاعدة بياناتك التي أنشأتها. انقر على نافذة "نظرة عامة Overview" للعودة إلى شاشة نظرة عامة على العنقود. عاشرًا، انقر على زر "اتصال Connect" من شاشة نظرة عامة Overview للعنقود Cluster0. سيؤدي ذلك إلى فتح شاشة الاتصال بالعنقود Connect to Cluster. انقر على خيار Drivers الموجود تحت خيار الاتصال بتطبيقك Connect your application. أخيرًا، ستظهر لك شاشة الاتصال Connect. اتبع بعد ذلك الخطوات التالية: حدد مشغّل driver ونسخة Node كما هو موضح في الشكل السابق. انقر على أيقونة النسخ Copy لنسخ سلسلة الاتصال. الصقها في محرر نصوصك المحلي. حدّث اسم المستخدم وكلمة المرور بكلمة مرور مستخدمك. أدخِل اسم قاعدة البيانات "local_library" في المسار قبل الخيارات (...mongodb.net/local_library?retryWrites...). احفظ الملف الذي يحتوي على هذه السلسلة في مكان آمن. أنشأتَ قاعدة البيانات، ولديك عنوان URL (مع اسم مستخدم وكلمة مرور) الذي يمكن استخدامه للوصول إليها، إذ سيبدو كما يلي: mongodb+srv://your_user_name:your_password@cluster0.lz91hw2.mongodb.net/local_library?retryWrites=true&w=majority تثبيت Mongoose افتح موجّه الأوامر وانتقل إلى المجلد الذي أنشأتَ فيه موقعك الهيكلي الخاص بالمكتبة المحلية، ثم أدخِل الأمر التالي لتثبيت مكتبة Mongoose (واعتمادياتها) وضِفها إلى ملف package.json، إن لم تكن قد فعلتَ ذلك مسبقًا عند قراءة فقرة مقدمة إلى مكتبة Mongoose. npm install mongoose الاتصال بقاعدة بيانات MongoDB افتح الملف "/app.js" في جذر مشروعك وانسخ النص التالي في مكان التصريح عن كائن تطبيق Express (بعد سطر const app = express();). ضع عنوان URL الخاص بالموقع الذي يمثل قاعدة بياناتك (أي باستخدام المعلومات الواردة من mongoDB Atlas) مكان سلسلة عنوان URL لقاعدة البيانات ('insert_your_database_url_here'). // إعداد اتصال mongoose const mongoose = require("mongoose"); mongoose.set("strictQuery", false); const mongoDB = "insert_your_database_url_here"; main().catch((err) => console.log(err)); async function main() { await mongoose.connect(mongoDB); } تنشئ هذه الشيفرة البرمجية الاتصال الافتراضي بقاعدة البيانات ويبلّغ عن وجود أيّ أخطاء في الطرفية. تعريف مخطط المكتبة المحلية سنعرّف وحدة منفصلة لكل نموذج كما وضّحنا سابقًا. ابدأ بإنشاء مجلد للنماذج في جذر المشروع (/models) ثم أنشئ ملفات منفصلة لكل نموذج كما يلي: /express-locallibrary-tutorial // the project root /models author.js book.js bookinstance.js genre.js نموذج المؤلف Author انسخ شيفرة مخطط المؤلف Author التالية والصقها في ملف "./models/author.js"، إذ يعرّف هذا المخطط مؤلفًا يحتوي على حقول من نوع المخطط String للاسم الأول واسم العائلة (مطلوبة بحد أقصى 100 محرف) وحقول من النوع Date لتواريخ الميلاد والوفاة. const mongoose = require("mongoose"); const Schema = mongoose.Schema; const AuthorSchema = new Schema({ first_name: { type: String, required: true, maxLength: 100 }, family_name: { type: String, required: true, maxLength: 100 }, date_of_birth: { type: Date }, date_of_death: { type: Date }, }); // الخاصية الافتراضية لاسم المؤلف الكامل AuthorSchema.virtual("name").get(function () { // يمكن تجنب الأخطاء في الحالات التي لا يحمل فيها المؤلف اسم عائلة أو اسمًا أولًا // من خلال التأكد من معالجة الاستثناء عبر إعادة سلسلة فارغة لهذه الحالة let fullname = ""; if (this.first_name && this.family_name) { fullname = `${this.family_name}, ${this.first_name}`; } return fullname; }); // الخاصية الافتراضية لعنوان URL الخاص بالمؤلف AuthorSchema.virtual("url").get(function () { // لا نستخدم دالة سهمية لأننا نحتاج إلى هذا الكائن return `/catalog/author/${this._id}`; }); // تصدير النموذج module.exports = mongoose.model("Author", AuthorSchema); صرّحنا عن الخاصية الافتراضية لمخطط المؤلف AuthorSchema بالاسم "url"، والتي تعرض عنوان URL المطلق المطلوب للحصول على نسخة معينة من النموذج، إذ سنستخدم هذه الخاصية في قوالبنا كلما احتجنا إلى الحصول على ارتباط إلى مؤلف معين. ملاحظة: يُعَد التصريح عن عناوين URL بوصفها خاصيات افتراضية في المخطط فكرةً جيدة لأن عنوان URL لعنصرٍ ما يجب تغييره في مكانٍ واحد فقط. لن يعمل الرابط الذي يستخدم عنوان URL هذا حاليًا، لأنه ليس لدينا أيّ شيفرة لمعالجة الوجهات Routes لنسخ النماذج الفردية، إذ سنضبطها في مقالٍ لاحق. نصدّر بعد ذلك النموذج في نهاية الوحدة. نموذج الكتاب Book انسخ شيفرة مخطط الكتاب Book التالية والصقها في الملف "./models/book.js"، والتي تشبه في معظمها نموذج المؤلف، إذ صرّحنا عن مخطط يحتوي على عدد من الحقول من النوع String وخاصية افتراضية للحصول على عنوان URL لسجلات كتاب معينة، ثم صدّرنا النموذج. const mongoose = require("mongoose"); const Schema = mongoose.Schema; const BookSchema = new Schema({ title: { type: String, required: true }, author: { type: Schema.Types.ObjectId, ref: "Author", required: true }, summary: { type: String, required: true }, isbn: { type: String, required: true }, genre: [{ type: Schema.Types.ObjectId, ref: "Genre" }], }); // الخاصية الافتراضية لعنوان URL الخاص بالكتاب BookSchema.virtual("url").get(function () { // لا نستخدم دالة سهمية لأننا نحتاج إلى هذا الكائن return `/catalog/book/${this._id}`; }); // تصدير النموذج module.exports = mongoose.model("Book", BookSchema); الاختلاف الرئيسي هنا هو أننا أنشأنا مرجعين إلى نماذج أخرى هما: المؤلف author هو مرجع إلى كائن نموذج Author واحد، وهو مطلوب. النوع genre هو مرجع إلى مصفوفة من كائنات نموذج Genre، ولكننا لم نصرّح عن هذا الكائن بعد. نموذج نسخة الكتاب BookInstance انسخ شيفرة مخطط BookInstance التالية والصقها في الملف "./models/bookinstance.js"، إذ يمثل BookInstance نسخةً محددةً من الكتاب الذي يمكن أن يستعيره شخص ما ويتضمن معلومات حول ما إذا كانت النسخة متوفرة، والتاريخ المتوقع لاسترجاعها، وتفاصيل "الطبعة" أو النسخة. const mongoose = require("mongoose"); const Schema = mongoose.Schema; const BookInstanceSchema = new Schema({ book: { type: Schema.Types.ObjectId, ref: "Book", required: true }, // reference to the associated book imprint: { type: String, required: true }, status: { type: String, required: true, enum: ["Available", "Maintenance", "Loaned", "Reserved"], default: "Maintenance", }, due_back: { type: Date, default: Date.now }, }); // الخاصية الافتراضية لعنوان URL الخاص بالكتاب BookInstanceSchema.virtual("url").get(function () { // لا نستخدم دالة سهمية لأننا نحتاج إلى هذا الكائن return `/catalog/bookinstance/${this._id}`; }); // تصدير النموذج module.exports = mongoose.model("BookInstance", BookInstanceSchema); الأشياء الجديدة هي خيارات الحقول التالية: enum: يسمح بضبط القيم المسموح بها من نوع السلسلة النصية String، إذ نستخدمه في هذه الحالة لتحديد حالة توفر الكتب. يعني استخدام enum أنه يمكننا منع الأخطاء الإملائية والقيم العشوائية للحالة. default: نستخدمه لضبط الحالة الافتراضية لنسخ الكتاب التي أنشأناها على القيمة "في الصيانة Maintenance" وتاريخ due_back الافتراضي على القيمة now. لاحظ كيفية استدعاء دالة التاريخ Date عند ضبط التاريخ. يجب أن يكون كل شيء آخر مألوفًا من المخطط السابق. نموذج نوع الكتاب Genre- التحدي افتح الملف "./models/genre.js" وأنشئ مخططًا لتخزين أنواع الكتب (فئة الكتاب مثل ما إذا كان كتابًا خياليًا أو غير خيالي أو عاطفيًا أو تاريخيًا عسكريًا وغير ذلك). سيكون التعريف مشابهًا جدًا للنماذج الأخرى: يجب أن يحتوي النموذج على نوع المخطط String بالاسم name لوصف نوع الكتاب. يجب أن يكون هذا الاسم مطلوبًا ويتكون من 3 إلى 100 محرف. التصريح عن الخاصية الافتراضية لعنوان URL الخاص بنوع الكتاب بالاسم url. تصدير النموذج. إنشاء بعض العناصر للاختبار لدينا الآن جميع النماذج المُعدَّة للموقع، ويمكننا اختبار النماذج وإنشاء بعض أمثلة الكتب والعناصر الأخرى التي يمكننا استخدامها في المقالات اللاحقة من خلال تشغيل سكربت مستقل لإنشاء عناصر لكل نوع. أولًا، نزّل أو أنشئ الملف populatedb.js ضمن المجلد express-locallibrary-tutorial (في مستوى الملف package.json نفسه). ملاحظة: لست بحاجة إلى معرفة كيفية عمل الملف "populatedb.js"، فهو يضيف فقط عينة بيانات إلى قاعدة البيانات. ثانيًا، شغّل السكريبت باستخدام أمر node في موجه أوامرك مع تمرير عنوان URL لقاعدة بيانات MongoDB (عنوان URL نفسه الذي وضعته مكان العنصر البديل insert_your_database_url_here في الملف app.js سابقًا): node populatedb <your mongodb url> ملاحظة: يجب تغليف عنوان URL لقاعدة البيانات ضمن علامات اقتباس (") مزدوجة في نظام ويندوز، ويمكن أن تحتاج إلى علامات اقتباس مفردة (') في أنظمة التشغيل الأخرى. ثالثًا، يجب تشغيل السكريبت حتى اكتماله، إذ يعرض العناصر أثناء إنشائها في الطرفية. ملاحظة: انتقل إلى قاعدة بياناتك على MongoDB Atlas في نافذة التجميعات Collections، إذ يجب أن تكون الآن قادرًا على التنقل في مجموعات الكتب والمؤلفين والأنواع ونسخ الكتب والتحقق من المستندات الفردية. الخلاصة تعلمنا في هذا المقال بعض الأشياء عن قواعد البيانات ونماذج ORM على Node/Express، وتعرّفنا على كيفية تعريف مخطط ونماذج Mongoose، ثم استخدمنا هذه المعلومات لتصميم وتقديم نماذج Book و BookInstance و Author و Genre لموقع المكتبة المحلية LocalLibrary، واختبرنا نماذجنا من خلال إنشاء عدد من نسخ باستخدام سكريبت مستقل. سنتعرّف في المقال التالي على إنشاء بعض الصفحات لعرض هذه الكائنات. ترجمة -وبتصرُّف- للمقال Express Tutorial Part 3: Using a Database with Mongoose. اقرأ أيضًا المقال السابق تطبيق عملي لتعلم Express - الجزء الأول: إنشاء موقع ويب هيكلي لمكتبة محلية مقارنة بين MySQL و MongoDB تجهيز قاعدة البيانات PostgreSQL والتّعريف بمفهومي ORM وإضافات Flask دمج قاعدة البيانات MongoDB في تطبيقك Node
-
يشرح هذا المقال ما ستتعلمه لبناء موقع ويب باستخدام إطار عمل Express، ويوفر نظرةً عامة على مثال موقع المكتبة المحلية الذي سنعمل عليه ونطوّره في المقالات اللاحقة، وسنوضّح كيفية إنشاء مشروع موقع ويب هيكلي يمكنك ملؤه لاحقًا بالوجهات Routes والعروض Views أوالقوالب Templates واستدعاءات قاعدة البيانات الخاصة بالموقع. المتطلبات الأساسية: قراءة مدخل إلى إطار عمل Express،وإعداد بيئة تطوير Node مع Express. الهدف: مقدمة إلى التطبيق العملي الذي سنبنيه في المقالات اللاحقة، وفهم الموضوعات التي سنتناولها، والقدرة على بدء مشاريعك لمواقع الويب الجديدة باستخدام مولّد تطبيقات Express. سنطور موقع ويب يمكن استخدامه لإدارة دليلٍ لمكتبة محلية، وسنتعلم في هذه السلسلة من المقالات ما يلي: استخدام أداة مولّد تطبيقات Express لإنشاء موقع ويب وتطبيق هيكلي. بدء وإيقاف خادم ويب Node. استخدم قاعدة بيانات لتخزين بيانات تطبيقك. إنشاء الوجهات لطلب معلومات مختلفة، وقوالب أو عروض لعرض البيانات بتنسيق HTML لعرضها في المتصفح. العمل مع الاستمارات Forms. نشر تطبيقك في بيئة الإنتاج. تعلّمت مسبقًا عن بعض هذه الموضوعات، وتعرّفت إلى بعضها الآخر بإيجاز، ولكن يجب أن تعرف ما يكفي لتطوير تطبيقات Express بنفسك في نهاية هذه السلسلة من المقالات المتفرعة من سلسلة تعلم تطوير الويب. موقع المكتبة المحلية LocalLibrary LocalLibrary هو اسم موقع الويب الذي سننشئه ونطوّره في المقالات اللاحقة، والغرض الأساسي من هذا الموقع هو توفير دليل Catalog عبر الإنترنت لمكتبة محلية صغيرة، حيث يمكن للمستخدمين تصفح الكتب المتاحة وإدارة حساباتهم. اختير هذا المثال بعناية لأنه يمكن تغيير حجمه لإظهار الكثير أو القليل من التفاصيل التي نحتاجها، ويمكن استخدامه لإظهار أي ميزة من ميزات Express تقريبًا، ويسمح بتوفير مسار إرشادي عبر الوظائف التي ستحتاجها في أيّ موقع ويب كما يلي: سنعرّف في المقالات الأولى مكتبةً بسيطةً للتصفح فقط، إذ يمكن لأعضاء المكتبة استخدامها لمعرفة الكتب المتاحة، مما يتيح استكشاف العمليات المشتركة لكل مواقع الويب تقريبًا، وهي قراءة المحتوى وعرضه من قاعدة بيانات. يتوسّع مثال المكتبة في المقالات اللاحقة لإظهار ميزات موقع الويب الأكثر تقدمًا، فمثلًا يمكننا توسيع المكتبة للسماح بإنشاء كتب جديدة، واستخدام ذلك لتوضيح كيفية استخدام الاستمارات ودعم استيثاق Authentication المستخدمين. سُمِّي هذا المثال بمكتبة محلية لسببٍ ما بالرغم من أنه قابل جدًا للتوسّع، فهدفنا هو إظهار الحد الأدنى من المعلومات التي ستساعدك على بدء استخدام إطار عمل Express وتشغيله بسرعة، لذا سنخزّن معلومات عن الكتب ونسخها والمؤلفين والمعلومات الأساسية الأخرى، ولكن لن نخزّن معلومات حول العناصر الأخرى التي يمكن أن تعيرها المكتبة ولن نوفر البنية الأساسية اللازمة لدعم مواقع مكتبات متعددة أو ميزات مكتبة كبيرة أخرى. سنوفر في هذه السلسلة من المقالات مقتطفات من الشيفرة البرمجية المناسبة لك لنسخها ولصقها في كل مرحلة، مع وجود شيفرة برمجية أخرى نأمل أن توسّعها بنفسك مع بعض الإرشادات. حاول كتابة جميع مقتطفات الشيفرة البرمجية بدلًا من نسخها ولصقها، إذ سيفيدك ذلك على المدى الطويل إذ ستكون أكثر دراية في المرة القادمة التي تكتب فيها شيئًا مشابهًا. إذا واجهتك مشكلة، فيمكنك العثور على النسخة المُطوَّرة بالكامل من موقع الويب على GitHub. ملاحظة: توجد قائمة بالنسخ المحددة من Node و Express والوحدات الأخرى التي جرى اختبار هذه السلسلة من المقالات على أساسها في الملف package.json الخاص بالمشروع. حان الوقت لبدء إنشاء مشروع هيكلي بعد أن تعرّفت على موقع المكتبة المحلية LocalLibrary وما ستتعلمه في هذه السلسلة من المقالات. إنشاء موقع ويب هيكلي سنوضح الآن كيفية إنشاء موقع ويب هيكلي باستخدام أداة مولّد تطبيقات Express، والتي يمكنك بعد ذلك ملؤها بالوجهات والعروض أو القوالب واستدعاءات قاعدة البيانات الخاصة بالموقع، إذ سنستخدم هذه الأداة لإنشاء إطار عمل لموقع المكتبة المحلية الذي سنضيف إليه لاحقًا جميع الشيفرات البرمجية الأخرى التي يحتاجها الموقع. تُعَد هذه العملية بسيطة جدًا، وتتطلب فقط استدعاء المولّد في سطر الأوامر باسم مشروع جديد، وتحديد محرّك قوالب الموقع ومولد شيفرة CSS اختياريًا. توضّح الأقسام التالية كيفية استدعاء مولّد التطبيق، وتوفّر شرحًا بسيطًا حول خيارات العرض أو شيفرة CSS المختلفة، وسنشرح كيفية بناء موقع الويب الهيكلي، وسنوضح كيفية تشغيل موقع الويب للتحقق من أنه يعمل بنجاح. ملاحظة: لا يُعَد مولّد تطبيقات Express المولّد الوحيد لتطبيقات Express، ولا يُعَد المشروع المُولَّد الطريقة الوحيدة القابلة للتطبيق لبناء هيكيلية ملفاتك ومجلداتك، ولكن يحتوي الموقع المُولَّد على بنية معيارية يسهل توسيعها وفهمها. اطلع على مثال Hello world في توثيق Express لمزيد من المعلومات حول الحد الأدنى من تطبيقات Express. يصرّح مولّد تطبيقات Express عن معظم المتغيرات باستخدام var، ولكننا غيّرنا معظمها إلى const، وعددًا قليلًا منها إلى let، لأننا نريد عرض ممارسات جافا سكريبت Javascript الحديثة. يستخدم هذا المقال نسخة Express والاعتماديات الأخرى المُعرَّفة في الملف package.json التي أنشأها مولّد تطبيقات Express، إذ ليست بالضرورة أن تكون النسخة الأحدث، وقد ترغب في تحديثها عند نشر تطبيق حقيقي في بيئة الإنتاج. استخدام مولد التطبيق لا بد أنك ثبّتَ المولّد أثناء إعداد بيئة تطوير Node مع Express، ولكن يمكنك تثبيت أداة المولّد على مستوى الموقع باستخدام مدير حزم npm كما يلي: npm install express-generator -g يحتوي المولّد على عدد من الخيارات التي يمكنك عرضها في سطر الأوامر باستخدام الأمر --help (أو -h? > express --help Usage: express [options] [dir] Options: --version output the version number -e, --ejs add ejs engine support --pug add pug engine support --hbs add handlebars engine support -H, --hogan add hogan.js engine support -v, --view <engine> add view <engine> support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade) --no-view use static html instead of view engine -c, --css <engine> add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain CSS) --git add .gitignore -f, --force force on non-empty directory -h, --help output usage information يمكنك استخدام الأمر express لإنشاء مشروع ضمن المجلد الحالي باستخدام محرّك العروض Jade وشيفرة CSS، وإذا حدّدتَ اسم مجلد، فسيُنشَأ المشروع في مجلد فرعي بهذا الاسم. express يمكنك أيضًا اختيار محرك عروض أو قوالب باستخدام --view و/أو محرّك توليد شيفرة CSS باستخدام --css. ملاحظة: أهملنا الخيارات الأخرى لاختيار محركات القوالب مثل --hogan و --ejs و --hbs وغيرها، لذا استخدم الخيار --view أو -v محرك العروض الواجب استخدامه يتيح مولّد تطبيقات Express ضبطَ عددٍ من محرّكات العروض أو القوالب الشائعة، بما في ذلك EJS و Hbs و Pug (أو Jade) و Twig و Vash، ويختار Jade افتراضيًا إن لم تحدد خيار العرض، ويمكن أن يدعم Express أيضًا عددًا كبيرًا من لغات القوالب الأخرى. ملاحظة: إذا أردتَ استخدام محرك قوالب لا يدعمه المولّد، فاطلع على استخدام محركات القوالب مع Express في توثيق Express وتوثيق محرك العروض الذي تريد استخدامه. يجب تحديد محرّك القوالب الذي يوفّر جميع الوظائف التي تحتاجها ويسمح لك بالحصول على نتائج في أقرب وقت أو -بعبارة أخرى- باستخدام الطريقة نفسها التي تختار بها أي مكون آخر. إليك بعض الأشياء التي يجب مراعاتها عند الموازنة بين محركات القوالب: الوقت المستغرق للحصول على نتائج: إذا كان فريقك خبيرًا في استخدام لغة القوالب، فيُحتمَل أن يكونوا منتجين بصورة أسرع باستخدام تلك اللغة. إن لم يكن الأمر كذلك، فيجب التفكير في منحنى التعلّم النسبي لمحركات القوالب المُرشَّحة للاستخدام. الشعبية والنشاط: راجع شعبية المحرّك وما إذا كان لديه مجتمع نشط، فمن المهم أن تكون قادرًا على الحصول على الدعم عند ظهور مشاكل طوال مدة عمل الموقع. التنسيق: تستخدم بعض محركات القوالب شيفرة HTML التوصيفية للإشارة إلى المحتوى المُدخَل إلى شيفرة HTML عادية، بينما تبني محركات القوالب الأخرى شيفرة HTML باستخدام صيغة مختلفة (مثل استخدام المسافة البادئة وأسماء الكتل). الأداء أو الوقت اللازم للتقديم rendering time. الميزات: يجب أن تفكر فيما إذا كانت هذه المحركات توفّر الميزات التالية: توريث التخطيط Layout: يسمح بتعريف قالب أساسي ثم يمكنك وراثة أجزاء منه فقط وهي الأجزاء التي تريد أن تكون مختلفة لصفحة معينة، ويُعَد ذلك أسلوبًا أفضل من بناء القوالب من خلال تضمين عدد من المكونات المطلوبة أو بناء قالب من نقطة الصفر في كل مرة. دعم التضمين: يسمح ببناء قوالب من خلال تضمين قوالب أخرى. صيغة مُختصَرة للتحكم في المتغيرات والحلقات. القدرة على ترشيح قيم المتغيرات على مستوى القالب مثل جعل المتغيرات بأحرف كبيرة أو تنسيق قيمة التاريخ. القدرة على توليد تنسيقات الخرج بتنسيقات مختلفة عن تنسيق HTML مثل تنسيق JSON أو XML. دعم العمليات والتدفق غير المتزامن. ميزات من طرف العميل: إذا أمكن استخدام محرّك قوالب من طرف العميل، فسيسمح ذلك بإمكانية إجراء جميع أو معظم التقديم من طرف العميل. سنستخدم في مشروعنا محرك القوالب Pug (وهو محرك Jade الذي أُعيدت تسميته مؤخرًا) الذي يُعَد من أشهر لغات قوالب Express/جافا سكريبت ويدعمه المولّد. محرك تنسيق أو شيفرة CSS الذي يجب استخدامه يتيح مولّد تطبيقات Express إنشاء مشروع مضبوط لاستخدام أكثر محركات تنسيق أو شيفرة CSS شيوعًا مثل LESS و SASS و Compass و Stylus. ملاحظة: تحتوي لغة CSS على بعض القيود التي تصعّب إنجاز بعض المهام، ولكن تتيح محركات تنسيق أو شيفرة CSS استخدام صيغة أقوى لتعريف شيفرة CSS ثم تصريف التعريف في شيفرة CSS قديمة وبسيطة لتستخدمها المتصفحات. يجب أن تستخدم محرك تنسيقات يسمح لفريقك أن يكون أكثر إنتاجية، إذ سنستخدم في مشروعنا شيفرة CSS الصرفة (الافتراضية) لأن متطلبات شيفرة CSS ليست معقدة بما يكفي لاستخدام شيء آخر. قاعدة البيانات الواجب استخدامها لا تستخدم أو تضمّن الشيفرة البرمجية المُولَّدة أيّ قاعدة بيانات، ويمكن لتطبيقات Express استخدام أيّ آلية قاعدة بيانات تدعمها بيئة Node، إذ لا يحدد Express أي سلوك أو متطلبات إضافية محددة لإدارة قاعدة البيانات (سنناقش كيفية التكامل مع قاعدة بيانات في مقال لاحق). إنشاء المشروع سننشئ مشروعًا بالاسم "express-locallibrary-tutorial" باستخدام مكتبة قوالب Pug بدون محرّك شيفرة CSS. أولًا، انتقل إلى المكان الذي تريد إنشاء المشروع فيه ثم شغّل مولّد تطبيقات Express في موجّه الأوامر كما يلي: express express-locallibrary-tutorial --view=pug سينشئ المولّد ملفات المشروع ويسردها كما يلي: create : express-locallibrary-tutorial\ create : express-locallibrary-tutorial\public\ create : express-locallibrary-tutorial\public\javascripts\ create : express-locallibrary-tutorial\public\images\ create : express-locallibrary-tutorial\public\stylesheets\ create : express-locallibrary-tutorial\public\stylesheets\style.css create : express-locallibrary-tutorial\routes\ create : express-locallibrary-tutorial\routes\index.js create : express-locallibrary-tutorial\routes\users.js create : express-locallibrary-tutorial\views\ create : express-locallibrary-tutorial\views\error.pug create : express-locallibrary-tutorial\views\index.pug create : express-locallibrary-tutorial\views\layout.pug create : express-locallibrary-tutorial\app.js create : express-locallibrary-tutorial\package.json create : express-locallibrary-tutorial\bin\ create : express-locallibrary-tutorial\bin\www تغيير المجلد: > cd express-locallibrary-tutorial تثبيت الاعتماديات: > npm install تشغيل التطبيق (باستخدام Bash على نظام لينكس أو ماك) > DEBUG=express-locallibrary-tutorial:* npm start تشغيل التطبيق (باستخدام PowerShell على نظام ويندوز) > $ENV:DEBUG = "express-locallibrary-tutorial:*"; npm start تشغيل التطبيق (باستخدام موجّه الأوامر على نظام ويندوز) > SET DEBUG=express-locallibrary-tutorial:* & npm start يوفر المولّد في نهاية الخرج إرشادات حول كيفية تثبيت الاعتماديات (كما هو مذكور في الملف package.json) وكيفية تشغيل التطبيق على أنظمة تشغيل مختلفة. ملاحظة: تعرّف الملفات التي أنشأها المولّد جميع المتغيرات بوصفها var، لذا افتح جميع الملفات المُولَّدة وغيّر تصريحات var إلى const قبل المتابعة، إذ سنفترض لاحقًا أنك أجريت هذا التعديل. تشغيل الموقع الهيكلي لدينا حاليًا مشروع هيكلي كامل، ولا يجري موقع الويب الكثير من الأمور حتى الآن، لكن الأمر يستحق تشغيله لإثبات أنه يعمل بنجاح. أولًا، ثبّت الاعتماديات باستخدام الأمر install الذي يجلب جميع حزم الاعتماديات المُدرَجة في الملف package.json الخاص بالمشروع: cd express-locallibrary-tutorial npm install ثانيًا، شغّل التطبيق من خلال استخدام الأمر التالي في موجّه أوامر ويندوز CMD: SET DEBUG=express-locallibrary-tutorial:* & npm start واستخدم الأمر التالي في Powershell ضمن نظام ويندوز: ENV:DEBUG = "express-locallibrary-tutorial:*"; npm start ملاحظة: لم نشرح أوامر Powershell بصورة أكبر في هذا المقال، إذ تفترض أوامر ويندوز التي سنستخدمها أنك تستخدم موجّه أوامر ويندوز CMD. استخدم الأمر التالي في نظامي لينكس أو ماك أو إس: DEBUG=express-locallibrary-tutorial:* npm start ثالثًا، حمّل بعد ذلك العنوان http://localhost:3000/ في متصفحك للوصول إلى التطبيق. يجب أن تشاهد صفحة متصفح تشبه ما يلي: أصبح لديك الآن تطبيق Express يعمل بنجاح ويمكن الوصول إليه عبر المنفذ 3000. ملاحظة: يمكنك بدء تشغيل التطبيق فقط باستخدام الأمر npm start. يفعّل تحديد المتغير DEBUG كما هو موضح فيما يلي تسجيل الطرفية أو تنقيح الأخطاء، فمثلًا سترى خرج تنقيح الأخطاء التالي عندما تزور الصفحة السابقة: SET DEBUG=express-locallibrary-tutorial:* & npm start > express-locallibrary-tutorial@0.0.0 start D:\github\mdn\test\exprgen\express-locallibrary-tutorial > node ./bin/www express-locallibrary-tutorial:server Listening on port 3000 +0ms GET / 304 490.296 ms - - GET /stylesheets/style.css 200 4.886 ms - 111 تفعيل إعادة بدء الخادم عند إجراء تغييرات على الملف لا تظهر التغييرات التي تجريها على موقع Express الخاص بك حاليًا حتى تعيد تشغيل الخادم، ويصبح الأمر مزعجًا عندما تضطر إلى إيقاف وإعادة تشغيل الخادم في كل مرة تجري فيها تغييرًا، لذا فإن الأمر يستحق قضاء بعض الوقت لأتمتة إعادة بدء الخادم عند الحاجة. يمكنك استخدام أداة مناسبة لهذا الغرض وهي nodemon، وتُثبَّت عادةً على المستوى العام بوصفها أداةً، ولكننا سنثبّتها ونستخدمها في مثالنا محليًا بوصفها اعتماديةً للمطور، بحيث يحصل عليها أيّ مطور يعمل على المشروع تلقائيًا عند تثبيت التطبيق. استخدم الأمر التالي في المجلد الجذر للمشروع للهيكلي: npm install --save-dev nodemon إذا أردت تثبيت الأداة nodemon على المستوى العام في جهازك، وليس فقط على الملف package.json الخاص بمشروعك، فاستخدم الأمر التالي: npm install -g nodemon إذا فتحتَ الملف package.json الخاص بمشروعك، فسترى الآن قسمًا جديدًا مع الاعتمادية التالية: "devDependencies": { "nodemon": "^2.0.4" } بما أننا لم نثبّت الأداة على المستوى العام، فلا يمكننا تشغيلها من سطر الأوامر (إلا في حالة إضافتها إلى المسار)، ولكن يمكننا استدعاؤها من سكريبت npm، لأن مدير الحزم npm على علم بكل شيء عن الحزم المُثبَّتة. ابحث عن القسم scripts في الملف package.json الذي سيحتوي في البداية على سطر واحد يبدأ بكلمة "start"، لذا حدّثه بوضع فاصلة في نهاية هذا السطر، وضِف سطري "devstart" و "serverstart". سيبدو قسم السكربتات scripts كما يلي في نظامي لينكس وماك أو إس: "scripts": { "start": "node ./bin/www", "devstart": "nodemon ./bin/www", "serverstart": "DEBUG=express-locallibrary-tutorial:* npm run devstart" }, واستخدم الأمر التالي في نظام ويندوز: "serverstart": "SET DEBUG=express-locallibrary-tutorial:* & npm run devstart" يمكننا الآن بدء الخادم بالطريقة السابقة نفسها تمامًا ولكن باستخدام الأمر devstart. ملاحظة: إذا عدّلتَ الآن أيّ ملف في المشروع، فسيُعاد بدء الخادم، أو يمكنك إعادة بدء الخادم بكتابة rs في موجّه الأوامر في أيّ وقت، وستظل بحاجة إلى إعادة تحميل المتصفح لتحديث الصفحة. يجب الآن استدعاء الأمر "npm run <scriptname>" بدلًا من مجرد استخدام npm start، لأن "start" هو أمر npm يُربَط بالسكريبت. كان بإمكاننا استبدال الأمر في السكربت start ولكننا نريد فقط استخدام الأداة nodemon أثناء عملية التطوير، لذلك من المنطقي إنشاء أمر سكربت جديد. يُعَد الأمر serverstart المُضاف إلى قسم السكريبتات scripts في الملف package.json مثالًا جيدًا، إذ يعني استخدام هذا الأسلوب أنك لم تعد مضطرًا إلى كتابة أمر طويل لبدء الخادم. لاحظ أن الأمر المحدد المضاف إلى السكريبت يعمل على نظام ماك أو لينكس فقط. المشروع الناتج لنطّلع الآن على المشروع الذي أنشأناه. بنية المجلد يمتلك المشروع المُولَّد الآن بنية الملفات التالية (الملفات هي العناصر التي ليس أولها "/") بعد تثبيت الاعتماديات. يعرّف الملف package.json اعتماديات التطبيق ومعلومات أخرى، ويعرّف سكريبتًا لبدء التشغيل يستدعي نقطة الدخول إلى التطبيق التي هي ملف جافا سكربت "/bin/www"، ممّا يؤدي إلى إعداد معالجة أخطاء التطبيق ثم تحميل الملف app.js لينفّذ بقية العمل. تُخزَّن وجهات Routes التطبيق في وحدات منفصلة ضمن المجلد "/routes"، وتُخزَّن القوالب في المجلد "/views". express-locallibrary-tutorial app.js /bin www package.json package-lock.json /node_modules [about 6700 subdirectories and files] /public /images /javascripts /stylesheets style.css /routes index.js users.js /views error.pug index.pug layout.pug سنشرح في الأقسام التالية هذه الملفات بمزيد من التفصيل. الملف package.json يعرّف الملف package.json اعتماديات التطبيق ومعلومات أخرى كما يلي: { "name": "express-locallibrary-tutorial", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", "http-errors": "~1.6.3", "morgan": "~1.9.1", "pug": "2.0.0-beta11" }, "devDependencies": { "nodemon": "^2.0.4" } } تتضمن الاعتماديات الحزمة express وحزمة محرّك العروض المختار pug، وتوجد الحزم التالية المفيدة في العديد من تطبيقات الويب: cookie-parser: تُستخدَم لتحليل ترويسة ملف تعريف الارتباط Cookie وملء req.cookies الذي يوفّر تابعًا ملائمًا للوصول إلى معلومات ملفات تعريف الارتباط. debug: أداة مساعدة صغيرة لتنقيح أخطاء Node المُصمَّمة على غرار تقنية تنقيح أخطاء Node الأساسية. morgan: برمجية وسيطة لتسجيل طلبات HTTP خاصة ببيئة Node. http-errors: تنشئ أخطاء HTTP عند الحاجة (لمعالجة أخطاء Express). يعرّف قسم السكريبتات scripts أولًا السكريبت "start"، وهو السكريبت الذي نستدعيه عند استدعاء الأمر npm start لبدء الخادم، إذ أضاف مولّد تطبيقات Express هذا السكربت. يمكنك أن ترى من تعريف السكريبت أنه يمثّل بداية ملف جافا سكريبت "./bin/www" باستخدام node. "scripts": { "start": "node ./bin/www", "devstart": "nodemon ./bin/www", "serverstart": "DEBUG=express-locallibrary-tutorial:* npm run devstart" }, يمكن استخدام السكريبتات devstart و serverstart لبدء الملف "./bin/www" نفسه باستخدام nodemon بدلًا من node. الملف www يُعَد الملف "/bin/www" نقطة الدخول إلى التطبيق، وأول شيء يفعله هو استدعاء الدالة require() لطلب نقطة الدخول الحقيقية إلى التطبيق (أي الملف app.js في جذر المشروع)، والذي يضبط ويعيد كائن تطبيق express(). #!/usr/bin/env node /** * اعتماديات الوحدة */ const app = require("../app"); ملاحظة: تُعَد الدالة require() دالة Node عامة تُستخدَم لاستيراد الوحدات إلى الملف الحالي. نحدد في مثالنا وحدة app.js باستخدام مسار نسبي ونحذف امتداد الملف الاختياري (.js). تُعِد بقية الشيفرة البرمجية في هذا الملف خادم HTTP الخاص ببيئة Node مع إعداد app على منفذ معين (مُحدَّد في متغير بيئة أو بالقيمة 3000 عند عدم تحديد هذا المتغير)، وتبدأ بالاستماع والإبلاغ عن أخطاء واتصالات الخادم. لا تحتاج حاليًا إلى معرفة أي شيء آخر عن الشيفرة البرمجية، فكل شيء في هذا الملف هو شيفرة متداولة، ولكن لا تتردد في الاطلاع عليها إن أردتَ ذلك. الملف app.js ينشئ هذا الملف كائن تطبيق express (يسمى app اصطلاحيًا)، ويُعِد التطبيق باستخدام إعدادات وبرمجيات وسيطة متنوعة مختلفة، ثم يصدّر التطبيق من الوحدة. توضّح الشيفرة البرمجية التالية أجزاء الملف التي تنشئ وتصدر كائن التطبيق: const express = require("express"); const app = express(); // … module.exports = app; وهو الكائن module.exports في ملف نقطة الدخول www كما ذكرنا سابقًا، حيث يُزوَّد المستدعِي بهذا الكائن عند استيراد هذا الملف. لنتعرّف على الملف app.js بالتفصيل. أولًا، نستورد بعض مكتبات Node المفيدة إلى الملف باستخدام الدالة require() بما في ذلك http-errors و express و morgan و cookie-parser التي نزّلناها مسبقًا لتطبيقنا باستخدام مدير حزم npm، ومكتبة path وهي مكتبة Node أساسية لتحليل مسارات الملفات والمجلدات. const createError = require("http-errors"); const express = require("express"); const path = require("path"); const cookieParser = require("cookie-parser"); const logger = require("morgan"); نطلب بعد ذلك باستخدام الدالة require() وحدات من مجلد الوجهات "routes"، إذ تحتوي هذه الوحدات أو الملفات على شيفرة برمجية للتعامل مع مجموعات معينة من "الوجهات" (مسارات URL). إذا أردنا توسيع التطبيق الهيكلي لسرد جميع الكتب في المكتبة مثلًا، فسنضيف ملفًا جديدًا للتعامل مع الوجهات المتعلقة بالكتب. const indexRouter = require("./routes/index"); const usersRouter = require("./routes/users"); ملاحظة: استوردنا الوحدة، ولم نستخدم وجهاتها حتى الآن، ولكن سيحدث ذلك لاحقًا. ننشئ بعد ذلك الكائن app باستخدام وحدة express المستوردة، ثم نستخدمها لإعداد محرك العروض (أو القوالب)، إذ أن هناك قسمان لإعداد المحرك هما: ضبط قيمة "views" لتحديد المجلد الذي ستُخزَّن القوالب فيه، وهو في حالتنا المجلد الفرعي "/views"، ثم ضبط قيمة "view engine" لتحديد مكتبة القوالب، وهي في حالتنا "pug". const app = express(); // إعداد محرّك العروض app.set("views", path.join(__dirname, "views")); app.set("view engine", "pug"); تستدعي المجموعة التالية من الدوال app.use() لإضافة مكتبات البرمجيات الوسيطة التي استوردناها سابقًا إلى سلسلة معالجة الطلبات، فمثلًا هناك حاجة إلى استخدام express.json() و express.urlencoded() لملء متن الطلب req.body بحقول الاستمارة. نستخدم أيضًا بعد هذه المكتبات البرمجيةَ الوسيطة express.static التي تجعل إطار عمل Express يخدّم جميع الملفات الثابتة في المجلد "/public" ضمن جذر المشروع. app.use(logger("dev")); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, "public"))); ضبطنا جميع البرمجيات الوسيطة الأخرى، وسنضيف الآن شيفرة معالجة الوجهات (التي استوردناها سابقًا) إلى سلسلة معالجة الطلبات، إذ ستعرّف الشيفرة المستوردة وجهات معينة لأجزاء مختلفة من الموقع: app.use("/", indexRouter); app.use("/users", usersRouter); ملاحظة: تُعامَل المسارات المحددة السابقة ('/' و '/users') بوصفها بادئةً للوِجهات المُعرَّفة في الملفات المستوردة، فمثلًا إذا عرّفت وحدةُ المستخدمين users المُستورَدة وجهة المسار /profile، فيمكنك الوصول إلى هذا الوجهة من خلال /users/profile (سنتحدث أكثر عن الوِجهات في مقال لاحق). تضيف البرمجيات الوسيطة الأخيرة في الملف توابع معالجة للأخطاء واستجابات HTTP 404: // التقاط 404 وتوجيه معالج الخطأ app.use((req, res, next) => { next(createError(404)); }); // معالج الخطأ app.use((err, req, res, next) => { // ضبط locals، وتوفير خطأ في عملية التطوير فقط res.locals.message = err.message; res.locals.error = req.app.get("env") === "development" ? err : {}; // تقديم صفحة الخطأ res.status(err.status || 500); res.render("error"); }); ضُبِط كائن تطبيق Express (أو app) كاملًا الآن، والخطوة الأخيرة هي إضافته إلى module.exports، مما يسمح بأن يستورده الملف /bin/www. module.exports = app; الوجهات اطّلع على ملف الوجهات "/routes/users.js" التالي، حيث تشترك ملفات الوجهات ببنية مماثلة، لذلك لا حاجة لعرض الملف index.js أيضًا. يحمّل هذا الملف أولًا الوحدة express ويستخدمها للحصول على الكائن express.Router، ثم يحدد وجهةً لهذا الكائن ويصدِّر الموجّه من الوحدة، مما يسمح باستيراد الملف في app.js. const express = require("express"); const router = express.Router(); /* الحصول على قائمة المستخدمين */ router.get("/", (req, res, next) => { res.send("respond with a resource"); }); module.exports = router; تعرّف الوجهة دالة رد نداء callback ستُستدعَى عند اكتشاف طلب HTTP من النوع GET بالنمط الصحيح، إذ يُعَد نمط المطابقة هو الوجهة المُحدَّدة عند استيراد الوحدة ('/users')، إضافةً إلى كل ما جرى تعريفه في هذا الملف ('/')، وبالتالي ستُستخدَم هذه الوجهة عند تلقي عنوان URL للمستخدمين /users/. ملاحظة: جرب ذلك من خلال تشغيل الخادم باستخدام node وزيارة عنوان URL في متصفحك http://localhost:3000/users/، ويجب أن ترى الرسالة "respond with a resource". تمتلك دالة رد النداء الوسيط الثالث next، وبالتالي هي دالة وسيطة بدلًا من كونها دالة رد نداء بسيطة للوجهة. لا تستخدم الشيفرة البرمجية الوسيط next حاليًا، ولكنه مفيدٌ مستقبلًا إذا أردتَ إضافة معالجات وجهات متعددة إلى مسار الوجهة '/'. العروض أو القوالب تُخزَّن العروض (أو القوالب) في المجلد "/views" (كما هو مُحدَّد في الملف app.js) وتُمنَح امتداد الملف .pug، ويُستخدَم التابع Response.render() لتصيير قالب محدد مع قيم المتغيرات المُسمَّاة الممرَّرة في كائن، ثم إرسال النتيجة بوصفها استجابة. يمكنك أن ترى في الشيفرة التالية من الملف "/routes/index.js" كيف تصيّر هذه الوجهة استجابةً باستخدام القالب "index" الذي يمرّر متغير القالب "title". /* الحصول على الصفحة الرئيسية */ router.get("/", (req, res, next) => { res.render("index", { title: "Express" }); }); القالب المقابل للوجهة السابقة مذكور فيما يلي (index.pug). سنتحدث أكثر عن الصيغة لاحقًا، ولكن كل ما تحتاج إلى معرفته الآن هو أن المتغير title (مع القيمة "Express") يُدرَج في المكان المُحدَّد في القالب. extends layout block content h1= title p Welcome to #{title} تحدى نفسك أنشئ وجهةً جديدة في الملف "/routes/users.js" تعرض النص "You're so cool" على العنوان /users/cool/، واختبرها من خلال تشغيل الخادم وزيارة العنوان http://localhost:3000/users/cool/ في متصفحك. الخلاصة أنشأتَ مشروعًا هيكليًا لموقع المكتبة المحلية وتحققتَ من أنه يعمل باستخدام node، وفهمتَ كيفية تنظيم بنية المشروع، لذلك أصبح لديك فكرةً جيدة عن مكان إجراء تغييرات لإضافة الوجهات والعروض للمكتبة المحلية. سنبدأ في المقال التالي بتعديل الموقع الهيكلي ليعمل بوصفه موقع ويب للمكتبة. ترجمة -وبتصرُّف- للمقالين Express Tutorial: The Local Library website و Express Tutorial Part 2: Creating a skeleton website. اقرأ أيضًا المقال السابق إعداد بيئة تطوير Node مع Express دليل استخدام Node.js وإطار العمل Express للمبتدئين تطبيق عملي لتعلم إطار عمل جانغو - الجزء الأول: إنشاء موقع ويب هيكلي لمكتبة محلية
-
عرفتَ من خلال المقال السابق الغرض من إطار عمل Express، وسنوضّح كيفية إعداد واختبار بيئة تطوير Node/Express على نظام ويندوز أو لينكس (أوبنتو) أو ماك أو إس macOS، إذ يوفر هذا المقال ما تحتاجه لبدء تطوير تطبيقات Express بالنسبة لأيّ نظام تشغيل من هذه الأنظمة. المتطلبات الأساسية: معرفة باستخدام سطر الأوامر أو الطرفية Terminal وكيفية تثبيت الحزم البرمجية على نظام تشغيل حاسوب التطوير خاصتك. الهدف: إعداد بيئة تطوير لإطار عمل Express على حاسوبك. نظرة عامة على بيئة تطوير Express تسهّل كلٌّ من بيئة Node وإطار عمل Express إعداد حاسوبك للبدء في تطوير تطبيقات الويب، إذ سنقدم في هذا القسم نظرةً عامة على الأدوات المطلوبة، وسنشرح بعض أبسط الطرق لتثبيت Node و Express على أوبنتو وماك أو اس macOS وويندوز، وسنوضح كيفية اختبار التثبيت. ما هي بيئة تطوير Express؟ تتضمن بيئة تطوير Express تثبيت بيئة Node.js ومدير الحزم npm ومولّد تطبيقات Express (اختياريًا) على حاسوبك المحلي. تُثبَّت بيئة Node ومدير الحزم npm مع بعضها من الحزم الثنائية المُعَدّة أو المثبِّتات Installers أو مديري حزم نظام التشغيل أو من المصدر (كما هو موضح في الأقسام التالية). يثبِّت مدير حزم npm إطار عمل Express بوصفه اعتمادية Dependency على تطبيقات ويب Express مع المكتبات الأخرى، مثل محركات القوالب ومشغّلات قواعد البيانات وبرمجيات الاستيثاق الوسيطة والبرمجيات الوسيطة لتخديم الملفات الثابتة وغير ذلك. يمكن أيضًا استخدام مدير حزم npm لتثبيت موّلد تطبيقات Express (على المستوى العام)، وهو أداة مفيدة لإنشاء تطبيقات Express الهيكلية التي تتبع نمط MVC. يُعَد مولّد التطبيقات اختياريًا لأنك لست بحاجة إلى استخدامه لإنشاء تطبيقات تستخدم إطار عمل Express أو بناء تطبيقات Express التي لها التخطيط المعماري أو الاعتماديات نفسها، ولكننا سنستخدمه لأنه يجعل البدء أسهل ويطوّر بنية التطبيق المعيارية. ملاحظة: لا تتضمن بيئة التطوير خادم ويب منفصل للتطوير على عكس بعض أطر عمل الويب الأخرى، إذ ينشئ ويشغّل تطبيق الويب خادمَ الويب الخاص به في Node/Express. هناك أدوات طرفية أخرى تشكل جزءًا من بيئة تطوير نموذجية بما في ذلك محررات النصوص أو بيئات التطوير المتكاملة IDE لتعديل الشيفرة البرمجية وأدوات إدارة التحكم بالشيفرة المصدرية مثل Git لإدارة النسخ المختلفة من شيفرتك البرمجية بأمان. نفترض أنك ثبَّتَ هذه الأنواع من الأدوات وبالأخص محرر النصوص. أنظمة التشغيل المدعومة يمكن تشغيل بيئة Node على أنظمة تشغيل ويندوز وماك أو اس macOS والعديد من إصدارات لينكس ودوكر وغير ذلك، وتوجد قائمة كاملة في صفحة تنزيلات Node.js. يجب أن يتمتع أي حاسوب شخصي تقريبًا بالأداء اللازم لتشغيل بيئة Node أثناء التطوير. يُشغَّل إطار عمل Express في بيئة Node، وبالتالي يمكن تشغيله على أي منصة تشغّل بيئة Node. سنقدم في هذا المقال تعليمات الإعداد لأنظمة ويندوز وماك macOS ولينكس (أوبنتو). نسخة Node/Express التي يجب استخدامها هناك العديد من إصدارات Node، إذ تحتوي الإصدارات الأحدث على إصلاحات للأخطاء ودعم للنسخ الأحدث من معايير ECMAScript (جافا سكريبت) وتحسينات على واجهة برمجة تطبيقات Node. يجب أن تستخدم الإصدار الأحدث من الإصدار المدعوم على المدى الطويل Long-term Supported -أو LTS اختصارًا، إذ سيكون هذا الإصدار أكثر استقرارًا من الإصدار الحالي مع وجود ميزات حديثة نسبيًا (ولا تزال صيانته موجودةً بنشاط)، ولكن يجب أن تستخدم الإصدار الحالي إذا كنت بحاجة إلى ميزة غير موجودة في نسخة LTS. يجب عليك دائمًا استخدام أحدث نسخة بالنسبة إلى إطار عمل Express. قواعد البيانات والاعتماديات الأخرى تُعَد الاعتماديات الأخرى -مثل مشغّلات قاعدة البيانات ومحرّكات القوالب ومحركات الاستيثاق وغير ذلك- جزءًا من التطبيق، وتُستورد إلى بيئة التطبيق باستخدام مدير الحزم npm (سنناقشها لاحقًا في مقالات خاصة بالتطبيق). تثبيت بيئة Node يجب أن تثبّتَ Nodejs ومدير حزم Node -أو npm اختصارًا- على نظام تشغيلك لاستخدام إطار عمل Express، ولتسهيل ذلك، سنثبّت أولًا مدير نسخ Node، ثم سنستخدمه لتثبيت أحدث النسخ المدعومة على المدى الطويل LTS من Node و npm. ملاحظة: يمكنك أيضًا تثبيت nodejs و npm باستخدام المثبِّتات المتوفرة على موقع Nodejs (اضغط على الزر لتنزيل إصدار LTS المُوصَى به لمعظم المستخدمين)، أو يمكنك التثبيت باستخدام مدير حزم نظام تشغيلك. نوصي جدًا باستخدام مدير نسخ Node لأنه يسهّل التثبيت والترقية والتبديل بين نسخ Node و npm. نظام ويندوز يوجد عدد من مديري نسخ Node لنظام تشغيل ويندوز، وسنستخدم في مثالنا nvm-windows الذي يحظى باحترام كبير بين مطوري Node. ثبّت أحدث نسخة باستخدام المثبِّت الذي تختاره من صفحة nvm-windows/releases، ثم افتح بعد تثبيت nvm-windows موجه الأوامر (أو PowerShell) وأدخِل الأمر التالي لتنزيل أحدث نسخة LTS من nodejs و npm: nvm install lts لنفترض أن نسخة LTS من nodejs هي 18.15.0، وبالتالي يمكنك ضبطها بوصفها النسخة الحالية للاستخدام باستخدام الأمر التالي: nvm use 18.15.0 ملاحظة: إذا تلقيت تحذيرات رفض الوصول "Access Denied"، فيجب تشغيل هذا الأمر في موجه الأوامر باستخدام أذونات المدير. استخدم الأمر nvm --help لمعرفة خيارات سطر الأوامر الأخرى مثل سرد جميع نسخ Node المتاحة وجميع نسخ NVM التي جرى تنزيلها. نظام تشغيل أوبنتو وماك أو إس macOS يوجد عدد من مديري نسخ Node لنظامي أوبنتو وماك أو إس، إذ يُعَد مدير نسخ nvm واحدًا من أكثرها شيوعًا، وهو النسخة الأصلية التي يعتمد عليها nvm-windows (اطلع على إرشادات الطرفية لتثبيت أحدث نسخة من nvm). ثبّت nvm، ثم افتح الطرفية وأدخِل الأمر التالي لتنزيل أحدث نسخة LTS من nodejs و npm: nvm install --lts لنفترض أن نسخة LTS من nodejs هي 18.15.0، إذ يعرض الأمر nvm list مجموعة النسخ الذي جرى تنزيلها والنسخة الحالية. يمكنك ضبط نسخة معينة بوصفها النسخة الحالية باستخدام الأمر التالي (أمر nvm-windows نفسه): nvm use 18.15.0 استخدم الأمر nvm --help لمعرفة خيارات سطر الأوامر الأخرى التي تكون مشابهةً أو مماثلةً لتلك التي يوفّرها nvm-windows. اختبار تثبيت Nodejs و npm يمكنك اختبار التثبيت بعد ضبط nvm لاستخدام نسخة Node معينة، إذ توجد طريقة جيدة لذلك وهي استخدام أمر النسخة "version" في موجه الأوامر او الطرفية والتحقق من إعادة سلسلة النسخة المتوقعة كما يلي: > node -v v18.15.0 يجب أيضًا تثبيت مدير حزم npm، ويمكن اختباره بالطريقة نفسها: > npm -v 9.3.1 لننشئ خادم Node نقي وبسيط يطبع الجملة "Hello World" في المتصفح عندما تزور عنوان URL الصحيح في متصفحك. أولًا، انسخ النص التالي إلى ملف بالاسم hellonode.js، إذ تستخدم الشيفرة التالية ميزات Node النقية بدون ميزات Express: // حمّل وحدة HTTP const http = require("http"); const hostname = "127.0.0.1"; const port = 3000; // أنشئ خادم HTTP واستمع إلى المنفذ 3000 للطلبات const server = http.createServer((req, res) => { // اضبط استجابة ترويسة HTTP بحالة HTTP ونوع المحتوى res.statusCode = 200; res.setHeader("Content-Type", "text/plain"); res.end("Hello World\n"); }); // الاستماع إلى الطلبات على المنفذ 3000، وتسجيل المنفذ الذي استمعنا إليه بوصفه دالة رد نداء server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); }); تستورد الشيفرة البرمجية وحدة http وتستخدمها لإنشاء خادم createServer() يستمع إلى طلبات HTTP على المنفذ 3000، ثم يطبع السكريبت رسالةً إلى الطرفية حول عنوان URL للمتصفح الذي يمكنك استخدامه لاختبار الخادم. تأخذ الدالة createServer() وسيطًا هو دالة رد النداء التي ستُستدعَى عند تلقي طلب HTTP، وتعيد هذه الدالة استجابة مع رمز حالة HTTP هو 200 ("OK") والنص "Hello World". ملاحظة: لا تقلق إن لم تفهم بالضبط ما تفعله هذه الشيفرة البرمجية حتى الآن، إذ سنشرحها بمزيد من التفصيل بمجرد أن نبدأ باستخدام Express. ثانيًا، ابدأ الخادم من خلال الانتقال إلى مجلد الملف hellonode.js نفسه في موجّه الأوامر، ثم استدعاء node مع اسم السكريبت كما يلي: >node hellonode.js Server running at http://127.0.0.1:3000/ ثالثًا، انتقل إلى العنوان http://127.0.0.1:3000. إذا عمل كل شيء بنجاح، فيجب أن يعرض المتصفح السلسلة النصية "Hello World". استخدام مدير الحزم npm يُعَد مدير حزم npm -إضافةً إلى بيئة Node- الأداة الأكثر أهمية للعمل مع تطبيقات Node، ويُستخدَم لجلب أيّ حزم (مكتبات جافا سكريبت) يحتاجها التطبيق للتطوير و/أو الاختبار و/أو الإنتاج، ويمكن أيضًا استخدامه لتشغيل الاختبارات والأدوات المستخدمة في عملية التطوير. ملاحظة: يُعَد إطار Express من منظور بيئة Node مجرد حزمة أخرى يجب تثبيتها باستخدام مدير الحزم npm ثم تطلبها في شيفرتك البرمجية. يمكنك استخدام مدير حزم npm يدويًا لجلب الحزم المطلوبة بصورة منفصلة، ويمكن إدارة الاعتماديات باستخدام ملف تعريف نصي بالاسم package.json، إذ يسرد هذا الملف جميع الاعتماديات الخاصة بحزمة جافا سكريبت معينة، بما في ذلك اسم الحزمة ونسختها ووصفها والملف الأولي المطلوب تنفيذه واعتماديات الإنتاج واعتماديات التطوير ونسخ Node التي يمكن أن العمل معها وغير ذلك. يجب أن يحتوي ملف package.json على كل شيء يحتاجه مدير حزم npm لجلب التطبيق وتشغيله، فإذا كنت بصدد كتابة مكتبة قابلة لإعادة الاستخدام، فيمكنك استخدام هذا التعريف لرفع حزمتك إلى مستودع npm وإتاحتها لمستخدمين آخرين. إضافة الاعتماديات توضح الخطوات التالية كيفية استخدام مدير حزم npm لتنزيل حزمة وحفظها في اعتماديات المشروع ثم طلبها في تطبيق Node. ملاحظة: سنعرض الإرشادات الخاصة بجلب حزمة Express وتثبيتها، وسنبيّن لاحقًا كيفية تحديد هذه الحزمة وغيرها باستخدام مولّد تطبيقات Express. يوفّر هذا القسم فهم كيفية عمل مدير حزم npm وما ينشئه مولّد التطبيق. أولًا، أنشئ مجلدًا لتطبيقك الجديد وانتقل إليه كما يلي: mkdir myapp cd myapp ثانيًا، استخدم أمر npm الذي هو init لإنشاء ملف package.json لتطبيقك. يطالبك هذا الأمر بعدد من الأشياء، بما في ذلك اسم ونسخة تطبيقك واسم ملف نقطة الدخول الأولي (وهو index.js افتراضيًا)، وما عليك حاليًا سوى قبول الإعدادات الافتراضية: npm init إذا عرضتَ ملف package.json (باستخدام الأمر cat package.json)، فسترى الإعدادات الافتراضية التي قبلتها وينتهي الملف بالترخيص License كما يلي: { "name": "myapp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ثالثًا، ثبّت Express في المجلد myapp واحفظه في قائمة الاعتماديات لملف package.json: npm install express سيظهر الآن قسم الاعتماديات "dependencies" الخاصة بملف package.json في نهايته وسيتضمن Express كما يلي: { "name": "myapp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.17.1" } } رابعًا، يمكن استخدام مكتبة Express من خلال استدعاء الدالة require() في الملف index.js لتضمينها في تطبيقك. أنشئ هذا الملف الآن في جذر مجلد التطبيق "myapp"، وضع فيه المحتويات التالية: const express = require("express"); const app = express(); const port = 3000; app.get("/", (req, res) => { res.send("Hello World!"); }); app.listen(port, () => { console.log(`Example app listening on port ${port}!`); }); تُظهِر هذه الشيفرة البرمجية أصغر تطبيق Express وهو "مرحبًا للعالم"، إذ تستورد هذه الشيفرة الوحدةَ "express" باستخدام الدالة require() وتستخدمها لإنشاء خادم (app) يستمع إلى طلبات HTTP على المنفذ 3000 ويطبع رسالة إلى الطرفية تشرح عنوان URL للمتصفح الذي يمكنك استخدامه لاختبار الخادم. تستجيب الدالة app.get() فقط لطلبات HTTP من النوع GET بمسار URL المحدد ('/') من خلال استدعاء دالة لإرسال رسالة Hello World!"" في مثالنا. ملاحظة: تسمح علامات الاقتباس المائلة في !{Example app listening on port ${port بإدخال قيمة $port في السلسلة النصية. خامسًا، يمكنك بدء تشغيل الخادم من خلال استدعاء Node مع السكربت في موجه الأوامر كما يلي: >node index.js سترى الخرج التالي على الطرفية: Example app listening on port 3000 سادسًا، انتقل إلى عنوان http://localhost:3000/، فإذا عمل كل شيء على ما يرام، فيجب أن يعرض المتصفح السلسلة النصية "Hello World!". تطوير الاعتماديات إذا اُستخدِمت الاعتمادية أثناء مرحلة التطوير فقط، فيجب حفظها بوصفها "اعتمادية تطوير" حتى لا يضطر مستخدمو الحزمة إلى تثبيتها في مرحلة الإنتاج، فمثلًا يمكنك استدعاء أمر npm التالي لاستخدام أداة تنقيح صياغة شيفرة جافا سكريبت الشهيرة ESLint: npm install eslint --save-dev ويمكنك الاطلاع على مقال تقييم صلاحية بيانات التطبيق واستخدام المدقق ESLint على أكاديمية حسوب لمزيد من المعلومات عن المدقق ESLint. سيُضاف بعد ذلك الإدخال التالي إلى ملف package.json الخاص بتطبيقك: "devDependencies": { "eslint": "^7.10.0" } ملاحظة: تُعَد منقحات الصياغة Linters أدوات تجري تحليلًا ثابتًا على البرمجيات للتعرف على الالتزام أو عدم الالتزام بمجموعة معينة من أفضل ممارسات كتابة الشيفرة البرمجية والإبلاغ عنها. تشغيل المهام يمكنك -إضافةً إلى تعريف وجلب الاعتماديات- تعريف السكريبتات المُسمَّاة في ملفات package.json واستدعاء مدير حزم npm لتنفيذها باستخدام الأمر run-script، إذ يُستخدَم هذا الأسلوب لأتمتة تشغيل الاختبارات وأجزاء من عملية التطوير أو إنشاء سلسلة أدوات، مثل أدوات التشغيل لتقليل شيفرة جافا سكريبت وتقليص الصور وتنقيح صياغة أو تحليل شيفرتك البرمجية وغير ذلك. ملاحظة: يمكن أيضًا استخدام مشغِّلات المهام مثل Gulp و Grunt لتشغيل الاختبارات والأدوات الخارجية الأخرى. يمكن أن نضيف مثلًا كتلة السكريبت التالية إلى ملف package.json (بافتراض أن شيفرة تطبيقنا المصدرية موجودة في المجلد /src/js) لتعريف سكريبت لتشغيل اعتمادية تطوير eslint التي حددناها في القسم السابق: "scripts": { // … "lint": "eslint src/js" // … } يُعَد eslint src/js أمرًا يمكننا إدخاله في سطر الأوامر أو في الطرفية لتشغيل eslint على ملفات جافا سكريبت الموجودة في المجلد src/js ضمن مجلد تطبيقنا. يوفّر تضمين ما ورد سابقًا ضمن الملف package.json الخاص بتطبيقنا اختصارًا لهذا الأمر وهو lint. يمكننا بعد ذلك تشغيل eslint باستخدام npm من خلال استدعاء ما يلي: npm run-script lint # أو (باستخدام الاسم البديل) npm run lint يمكن ألا يبدو المثال السابق أقصر من الأمر الأصلي، ولكن يمكنك تضمين أوامر أكبر بكثير ضمن سكريبتات npm بما في ذلك سلاسل أوامر متعددة، ويمكنك تحديد سكربت npm واحد يشغّل جميع اختباراتك مرة واحدة. تثبيت مولد تطبيقات Express تولّد أداة مولّد تطبيقات Express Application Generator تطبيق Express هيكلي، إذ يمكنك تثبيت هذا المولّد باستخدام npm كما يلي: npm install express-generator -g ملاحظة: يمكن أن تحتاج إلى أن تبدأ هذا السطر بالكلمة sudo على نظامي أوبنتو أو ماك أو إس، وتثبّت الراية -g الأداة بصورة عامة بحيث يمكنك استدعاؤها من أيّ مكان. يمكن إنشاء تطبيق Express بالاسم "helloworld" مع الإعدادات الافتراضية من خلال الانتقال إلى المكان الذي تريد إنشاءه فيه وتشغيل التطبيق كما يلي: express helloworld ملاحظة: يمكنك بدلًا مما سبق تخطي التثبيت وتشغيل express-generator باستخدام npx إن لم تستخدم نسخةً قديمة من nodejs أقل من 8.2.0، فالأمر التالي له التأثير نفسه للتثبيت ثم تشغيل express-generator ولكنه لا يثبّت الحزمة على نظامك: npx express-generator helloworld يمكنك أيضًا تحديد مكتبة القوالب المُراد استخدامها وعدد من الإعدادات الأخرى، لذا استخدم الأمر help لرؤية جميع هذه الخيارات. express --help سيُنشئ المولّد تطبيق Express الجديد في مجلد فرعي لموقعك الحالي، ويعرض تقدّم البناء على الطرفية، وستعرض الأداة عند الانتهاء الأوامر التي تحتاج إلى إدخالها لتثبيت اعتماديات Node وبدء التطبيق. سيحتوي التطبيق الجديد على ملف package.json في مجلده الجذر، ويمكنك فتحه لمعرفة الاعتماديات التي جرى تثبيتها، بما في ذلك Express ومكتبة القوالب Jade: { "name": "helloworld", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www" }, "dependencies": { "cookie-parser": "~1.4.4", "debug": "~2.6.9", "express": "~4.16.1", "http-errors": "~1.6.3", "jade": "~1.11.0", "morgan": "~1.9.1" } } ثبّت جميع الاعتماديات الخاصة بتطبيق helloworld باستخدام npm كما يلي: cd helloworld npm install شغّل التطبيق (تكون الأوامر مختلفة قليلًا لأنظمة تشغيل ويندوز ولينكس/ماك macOS) كما يلي: # شغّل تطبيق helloworld على ويندوز باستخدام موجّه الأوامر SET DEBUG=helloworld:* & npm start # شغّل تطبيق helloworld على ويندوز باستخدام PowerShell SET DEBUG=helloworld:* | npm start # شغّل تطبيق helloworld على لينكس/ماك macOS DEBUG=helloworld:* npm start ينشئ الأمر DEBUG تسجيلًا مفيدًا ينتج عنه خرج يشبه الخرج التالي: >SET DEBUG=helloworld:* & npm start > helloworld@0.0.0 start D:\GitHub\expresstests\helloworld > node ./bin/www helloworld:server Listening on port 3000 +0ms افتح متصفحك وانتقل إلى العنوان http://localhost:3000/ لمشاهدة صفحة ترحيب Express الافتراضية. سنتحدث أكثر عن التطبيق المُولَّد عندما ننتقل إلى المقال التالي. الخلاصة لديك الآن بيئة تطوير Node تعمل على حاسوبك والتي يمكن استخدامها لإنشاء تطبيقات ويب Express، وتعرّفت على كيفية استخدام npm لاستيراد Express في تطبيق وكيفية إنشاء تطبيقات باستخدام أداة مولّد تطبيقات Express ثم تشغيلها. نبدأ في المقال التالي العمل باستخدام تطبيق عملي لإنشاء تطبيق ويب كامل باستخدام هذه البيئة والأدوات المرتبطة بها. ترجمة -وبتصرُّف- للمقال Setting up a Node development environment. اقرأ أيضًا المقال السابق: مدخل إلى إطار عمل الويب Express وبية Node كتابة أول برنامج في بيئة Node.js وتنفيذه توثيق Node.js
-
يُعَد Express إطار عمل ويب شائع الاستخدام وغير مشتبث برأيه Unopinionated، أي لديه آراء حول الطريقة الصحيحة للتعامل مع أيّ مهمة معينة، ويدعم التطور السريع أو حل المشاكل في مجال معين، ومكتوب بلغة جافا سكريبت Javascript ومُستضاف في بيئة تشغيل Node.js. سنوضح في هذه السلسلة من المقالات المتفرعة عن السلسة الرئيسية تعلم تطوير الويب بعض الفوائد الرئيسية لإطار عمل Express، وكيفية إعداد بيئة التطوير وتطبيق مهام تطوير ونشر الويب الشائعة. المتطلبات الأساسية يجب قبل البدء التعرف على مفهوم برمجة الويب من طرف الخادم وأطر الويب من خلال الاطلاع على مقال مدخل إلى برمجة مواقع الويب من طرف الخادم. يوصَى بشدة بمعرفة مفاهيم البرمجة ولغة جافا سكريبت، ولكنها ليست ضرورية لفهم المفاهيم الأساسية. ملاحظة: اطلع على مقال أساسيات لغة جافا سكريبت في سياق التطوير من طرف العميل، إذ تعَد لغة ومفاهيم جافا سكريبت الأساسية هي نفسها بالنسبة للتطوير من طرف الخادم في بيئة Node.js. تقدم Node.js واجهات برمجة تطبيقات إضافية لدعم الوظائف المفيدة في البيئات التي لا تستخدم المتصفحات (لإنشاء خوادم HTTP والوصول إلى نظام الملفات مثلًا)، ولكنها لا تدعم واجهات برمجة تطبيقات جافا سكريبت للعمل مع المتصفحات ونموذج DOM. ستوفر هذه السلسلة من المقالات بعض المعلومات حول العمل مع بيئة Node.js وإطار عمل Express. تتألف هذه السلسلة من المقالات التالية: مدخل إلى إطار عمل الويب Express: نجيب في هذا المقال على أسئلة "ما هي بيئة Node؟" و"ما هو إطار عمل Express؟"، وسنأخذ فكرةً عامة على ما يجعل إطار عمل Express مميزًا، وسنحدد الميزات الرئيسية وسنعرض بعض البنى الأساسية لتطبيق Express، بالرغم من أنه لن يكون لديك بعد بيئة تطوير لاختبارها في هذه المرحلة. إعداد بيئة تطوير Node (في إطار عمل Express) (هذا المقال): سنوضح في هذا المقال كيفية إعداد واختبار بيئة تطوير Node/Express على أنظمة تشغيل ويندوز ولينكس (أوبنتو) وماك أو اس macOS. يجب أن يوفر لك هذا المقال ما تحتاجه لتتمكن من بدء تطوير تطبيقات Express مهما كان نظام التشغيل الذي تستخدمه. تطبيق عملي لتعلم Express - الجزء الأول: إنشاء موقع ويب هيكلي لمكتبة محلية: يشرح المقال الأول من التطبيق العملي لتعلم إطار عمل Express ما ستتعلمه، ويقدم نظرة عامة على مثال موقع "المكتبة المحلية" الذي سنعمل عليه ونطوره في المقالات اللاحقة. يوضح هذا المقال كيفية إنشاء مشروع موقع ويب هيكلي، والذي يمكنك ملؤه لاحقًا بالوجهات Routes والقوالب أو العروض وقواعد البيانات الخاصة بالموقع. تطبيق عملي لتعلم Express - الجزء الثاني: استخدام قاعدة البيانات (باستخدام مكتبة Mongoose): يوضح هذا المقال بإيجاز قواعد بيانات Node/Express، ثم يوضح كيفية استخدام مكتبة Mongoose لتوفير الوصول إلى قاعدة البيانات لموقع المكتبة المحلية LocalLibrary، ويشرح كيفية التصريح عن مخطط Schema الكائنات والنماذج وأنواع الحقول الرئيسية والتحقق من صحة البيانات الأساسي، ويعرض بإيجاز بعض الوجهات الرئيسية التي يمكنك من خلالها الوصول إلى بيانات النموذج. تطبيق عملي لتعلم Express - الجزء الثالث: الوجهات Routes والمتحكمات Controllers: سنُعِد في هذا المقال الوجهات (شيفرة معالجة عناوين URL) باستخدام دوال معالجة "وهمية dummy" لجميع النقاط النهائية للموارد التي سنحتاجها في موقع LocalLibrary. سيكون لدينا في النهاية بنية معيارية لشيفرة معالجة الوجهات، والتي يمكننا توسيعها باستخدام دوال معالجة حقيقية في المقالات اللاحقة. سنوضح أيضًا كيفية إنشاء وجهات معيارية باستخدام إطار عمل Express. تطبيق عملي لتعلم Express - الجزء الرابع: عرض بيانات المكتبة والعمل مع الاستمارات: سنضيف في هذا المقال الصفحات التي تعرض كتب موقع LocalLibrary وبيانات أخرى، حيث ستتضمن الصفحات صفحة رئيسية توضح عدد السجلات لكل نوع نموذج وصفحات القائمة والصفحات التفصيلية لجميع نماذجنا، وسنكتسب في هذا المقال خبرة عملية في الحصول على السجلات من قاعدة البيانات واستخدام القوالب. سنشرح أيضًا كيفية العمل مع استمارات HTML في إطار عمل Express باستخدام مكتبة Pug، وخاصة كيفية كتابة الاستمارات لإنشاء المستندات وتحديثها وحذفها من قاعدة البيانات. تطبيق عملي لتعلم Express - الجزء الخامس: النشر في بيئة الإنتاج: أنشأت موقع المكتبة المحلية LocalLibrary، ولكنك تريد تثبيته على خادم ويب عام حتى يصل إليه موظفو وأعضاء المكتبة عبر الإنترنت، لذا يقدّم هذا المقال نظرة عامة حول كيفية البحث عن مضيف لنشر موقعك، وما عليك فعله لتجهيز موقعك لمرحلة الإنتاج. هذه هي جميع المواضيع التي يجب تعلّمها، ولكن إذا أردتَ معرفة المزيد وتوسيع هذه السلسلة من المقالات، فاطلع على بعض الموضوعات الأخرى التي يجب تغطيتها وهي: استخدام الجلسات. استيثاق المستخدمين. الترخيص للمستخدمين وأذوناتهم. اختبار تطبيق Express. أمان الويب لتطبيقات Express. كما سيكون وجود تقييم نهائي إضافةً رائعة. سنجيب في هذا المقال على الأسئلة: "ما هي بيئة Node؟" و"ما هو إطار عمل Express؟"، ويقدّم نظرة عامة على ما يجعل إطار عمل Express مميزًا. سنحدد الميزات الرئيسية، وسنعرض بعض البنى الأساسية لتطبيق Express، بالرغم من أنه لن يكون لديك بعد بيئة تطوير لاختبارها في هذه المرحلة. المتطلبات الأساسية: المهارات الحاسوبية الأساسية، وفهم عام لبرمجة مواقع الويب من طرف الخادم وخاصةً تفاعلات الخادم مع العميل في مواقع الويب. الهدف: التعرف على إطار عمل Express وكيفية ملاءمته مع بيئة Node، والوظائف التي يوفرها، والمكونات الأساسية لتطبيق Express. مدخل إلى بيئة Node تُعَد Node -أو Node.js رسميًا- بيئة تشغيل مفتوحة المصدر ومتوافقة مع منصات مختلفة وتتيح للمطورين إنشاء جميع أنواع الأدوات والتطبيقات من طرف الخادم في شيفرة جافا سكريبت. يُعَد وقت التشغيل مخصَّصًا للاستخدام خارج سياق المتصفح، أي التشغيل مباشرةً على حاسوب أو نظام تشغيل خادم، وبالتالي تحذف البيئة واجهات برمجة تطبيقات جافا سكريبت الخاصة بالمتصفح وتضيف دعمًا لواجهات برمجة تطبيقات نظام التشغيل التقليدية بما في ذلك مكتبات HTTP ومكتبات نظام الملفات. تتمتع بيئة Node بعدد من الفوائد من منظور تطوير خادم الويب، وهي: أداء رائع: صُمِّمت بيئة Node لتحسين الإنتاجية وقابلية التوسع في تطبيقات الويب، وتُعَد حلًا جيدًا للعديد من مشاكل تطوير الويب الشائعة، مثل تطبيقات الويب في الوقت الفعلي. تُكتَب الشيفرة البرمجية باستخدام لغة جافا سكريبت قديمة وبسيطة، مما يقلل الوقت في التعامل مع "تغيير السياق" بين اللغات عند كتابة الشيفرة البرمجية من طرف العميل والخادم. تُعَد لغة جافا سكريبت لغة برمجة جديدة نسبيًا وتستفيد من التحسينات في تصميم اللغة عند مقارنتها مع لغات خادم الويب التقليدية الأخرى، مثل بايثون و PHP وغير ذلك. تُصرَّف أو تُحوَّل العديد من اللغات الجديدة والشائعة إلى لغة جافا سكريبت حتى يمكنك استخدام لغات TypeScript و CoffeeScript و ClojureScript و Scala و LiveScript وغيرها. يوفر مدير حزم Node -أو اختصارًا npm- الوصول إلى مئات الألوف من الحزم القابلة لإعادة الاستخدام، ولديه التحليل Resolution الأفضل للاعتماديات Dependency Resolution، ويمكن استخدامه لأتمتة معظم سلسلة أدوات البناء. تُعَد بيئة Node.js قابلة للنقل، فهي متوفرة على أنظمة تشغيل مايكروسوفت ويندوز وماك أو اس macOS ولينكس و Solaris و FreeBSD و OpenBSD و WebOS و NonStop OS، ويدعمها العديد من مزودي خدمات استضافة الويب الذين يوفرون في أغلب الأحيان بنية تحتية وتوثيقًا محددًا لاستضافة مواقع Node. تمتلك نظام بيئي ومجتمع مطورين خارجي نشط جدًا بوجود الكثير من الأشخاص المستعدين للمساعدة. يمكنك استخدام بيئة ب Node.js لإنشاء خادم ويب بسيط باستخدام حزمة Node HTTP. مرحبا Node.js ينشئ المثال التالي خادم ويب يستمع إلى أيّ نوع من طلبات HTTP على عنوان URL هو http://127.0.0.1:8000/، إذ سيستجيب السكريبت بالسلسلة النصية "Hello World" عند تلقي طلبٍ ما. إذا كانت بيئة Node مثبَّتةً لديك مسبقًا، فيمكنك اتباع الخطوات التالية لتجربة مثالنا: أولًا، افتح الطرفية، أو افتح الأداة المساعدة لسطر الأوامر على نظام ويندوز. ثانيًا، أنشئ المجلد الذي تريد حفظ البرنامج فيه مثل المجلد "test-node"، ثم انتقل إليه من خلال إدخال الأمر التالي في الطرفية: cd test-node ثالثًا، أنشئ ملفًا بالاسم "hello.js" وضع فيه الشيفرة البرمجية التالية باستخدام محرر النصوص المفضل لديك: // حمّل وحدة HTTP const http = require("http"); const hostname = "127.0.0.1"; const port = 8000; // أنشئ خادم HTTP const server = http.createServer(function (req, res) { // اضبط ترويسة استجابة HTTP بحالة HTTP ونوع المحتوى res.writeHead(200, { "Content-Type": "text/plain" }); // أرسل متن الاستجابة "Hello World" res.end("Hello World\n"); }); // اطبع سجلًا بمجرد أن يبدأ الخادم بالاستماع server.listen(port, hostname, function () { console.log(`Server running at http://${hostname}:${port}/`); }); رابعًا، احفظ الملف في المجلد الذي أنشأته سابقًا. خامسًا، ارجع إلى الطرفية واكتب الأمر التالي: node hello.js أخيرًا، انتقل إلى العنوان http://localhost:8000 في متصفح الويب، إذ يجب أن ترى النص "Hello World" في الجزء العلوي الأيسر من صفحة ويب أخرى فارغة. أطر عمل الويب لا تدعم بيئة Node مهام تطوير الويب الشائعة الأخرى مباشرةً بنفسها، فإذا أدرتَ إضافة معالجة محددة لأفعال HTTP المختلفة، مثل GET و POST و DELETE وغير ذلك، فعالج الطلبات بصورة منفصلة في مسارات URL المختلفة ("الوجهات Routes")، أو خدّم الملفات الثابتة، أو استخدم القوالب Templates لإنشاء الاستجابة ديناميكيًا، إذ لن تكون بيئة Node ذات فائدة كبيرة من تلقاء نفسها. يجب عليك إما كتابة الشيفرة البرمجية بنفسك، أو يمكنك تجنب إعادة اختراع العجلة واستخدام إطار عمل ويب. مقدمة إلى إطار عمل Express يُعَد Express إطار عمل الويب الخاص ببيئة Node الأكثر شيوعًا، وهو المكتبة الأساسية لعدد من أطر عمل الويب الخاصة ببيئة Node الشائعة الأخرى، ويوفر آليات بهدف: كتابة معالجات للطلبات ذات أفعال HTTP مختلفة في وجهات URL المختلفة. التكامل مع محرّكات تقديم "العرض View" لتوليد استجابات من خلال إدخال بيانات في القوالب. ضبط إعدادات تطبيقات الويب الشائعة مثل المنفذ المُستخدَم للاتصال وموقع القوالب المستخدمة لتقديم الاستجابة. إضافة برمجيات وسيطة middleware لمعالجة الطلبات الإضافية في أيّ وقت ضمن خط معالجة الطلبات. يُعَد إطار عمل Express بسيط إلى حدٍ ما، ولكن أنشأ المطورون حزمًا وسيطة متوافقة لمعالجة أيّ مشكلة في تطوير الويب تقريبًا، وتوجد مكتبات للعمل مع ملفات تعريف الارتباط والجلسات وتسجيل دخول المستخدمين ومعاملات عنوان URL وبيانات POST وترويسات الأمان وغير ذلك الكثير. اطلع على قائمة الحزم الوسيطة التي يهتم بها فريق Express مع قائمة ببعض الحزم الخارجية الشائعة. ملاحظة: تُعَد هذه المرونة سيفًا ذا حدين، إذ توجد حزم وسيطة لمعالجة أيّ مشكلة أو متطلب تقريبًا، ولكن يمكن أن يكون العمل على استخدام الحزم المناسبة تحديًا في بعض الأحيان. كما لا توجد طريقة صحيحة لبناء تطبيق، إذا لا تُعَد العديد من الأمثلة التي تجدها على الإنترنت مثالية، أو يمكن أن تعرض فقط جزءًا صغيرًا مما تحتاج إلى فعله لتطوير تطبيق ويب. تاريخ بيئة Node وإطار عمل Express أُصدِرت بيئة Node في البداية لنظام لينكس فقط في عام 2009، وأُصدِر مدير الحزم npm في عام 2010، وأُضيف دعم ويندوز الأصلي في عام 2012. أُصدِر إطار عمل Express في البداية في الشهر 11 من عام 2010 وهو حاليًا في الإصدار الرئيسي الرابع من واجهة برمجة التطبيقات API، ويمكنك التحقق من سجل التغييرات للحصول على معلومات حول التغييرات في الإصدار الحالي و GitHub للحصول على ملاحظات الإصدارات التاريخية الأكثر تفصيلًا. ما مدى شعبية Node و Express؟ تُعد شعبية إطار عمل الويب أمرًا مهمًا لأنه مؤشر على استمرار صيانته والموارد التي يُحتمَل أن تكون متاحة من حيث التوثيق والمكتبات الإضافية والدعم الفني. لا يوجد أيّ مقياس نهائي ومتوفر حاليًا لشعبية أطر العمل من طرف الخادم، بالرغم من أنه يمكنك تقدير الشعبية باستخدام آليات مثل حساب عدد مشاريع غيت هَب GitHub وأسئلة موقع StackOverflow لكل منصة. لذا يُفضَّل أن تسأل ما إذا كان Node و Express يتمتعان بشعبية كافية لتجنب مشاكل المنصات التي لا تحظى بشعبية، وما إذا كانا مستمرين في التطور ويمكنك الحصول على المساعدة إن احتجت إليها، وإذا كان هناك فرصة لك للحصول على عمل مدفوع الأجر إذا تعلمت Express. يمكن القول أن Express إطار عمل شائع الاستخدام استنادًا إلى عدد الشركات البارزة التي تستخدمه وعدد الأشخاص المساهمين في الشيفرة البرمجية الأساسية وعدد الأشخاص الذين يقدمون الدعم المجاني والمدفوع. هل Express إطار عمل قائم على رأيه Opinionated؟ تشير أطر عمل الويب إلى نفسها في أغلب الأحيان على أنها "قائمة على رأيها Opinionated" أو أنها "غير قائمة على رأيها Unopinionated"، فأطر العمل القائمة على رأيها هي أطر العمل التي لديها آراء حول الطريقة الصحيحة للتعامل مع أيّ مهمة معينة، وتدعم التطور السريع في نطاق محدد أو حل مشاكل من نوع معين لأن الطريقة الصحيحة لفعل أي شيء تكون عادةً مفهومة ومُوثَّقة جيدًا. لكن يمكن أن تكون أقل مرونة في حل المشاكل خارج مجالها الرئيسي، وتميل إلى تقديم خيارات أقل للمكونات والأساليب التي يمكن أن تستخدمها. يكون لأطر العمل غير القائمة على رأيها قيود أقل بكثير على أفضل طريقة لربط المكونات مع بعضها بعضًا لتحقيق هدفٍ ما أو حتى لتحديد المكونات التي يجب استخدامها، وتسهّل على المطورين استخدام أنسب الأدوات لإكمال مهمة معينة وإن كان ذلك على حساب الجهد التي تحتاجه للعثور على تلك المكونات بنفسك. يُعَد إطار عمل Express غير قائم على رأيه، إذ يمكنك إدخال أيّ برمجية وسيطة متوافقة تريدها تقريبًا إلى سلسلة معالجة الطلبات وبأيّ ترتيب تريده تقريبًا، ويمكنك بناء التطبيق في ملف واحد أو في ملفات متعددة وباستخدام أي بنية مجلدات، إذ ستشعر أحيانًا أن لديك الكثير من الخيارات. كيف تبدو شيفرة Express البرمجية؟ ينتظر تطبيق الويب طلبات HTTP من متصفح الويب (أو عميل آخر) في موقع ويب تقليدي مُوجَّه بالبيانات. يعمل التطبيق عند تلقي طلب على تحديد الإجراء المطلوب بناءً على نمط عنوان URL وربما على المعلومات المرتبطة به الواردة في بيانات POST أو بيانات GET، ثم يمكنه بعد ذلك -اعتمادًا على ما هو مطلوب- قراءة المعلومات أو كتابتها في قاعدة بيانات أو أداء مهام أخرى مطلوبة لتلبية الطلب، ثم سيعيد التطبيق استجابةً إلى متصفح الويب، وينشئ غالبًا صفحة HTML ديناميكيًا ليعرضها المتصفح من خلال إدخال البيانات المُسترجَعة ضمن عناصر بديلة في قالب HTML. يوفر Express توابعًا لتحديد الدالة المستدعاة لفعل HTTP معين، مثل GET و POST و SET وغير ذلك ونمط عنوان URL ("الوِجهة Route")، وتوابعًا لتحديد محرّك القالب ("العرض View") المستخدَم، ومكان وجود ملفات القالب، والقالب الذي يجب استخدامه لتقديم الاستجابة. يمكنك استخدام برمجيات Express الوسيطة لإضافة الدعم لملفات تعريف الارتباط والجلسات والمستخدمين والحصول على معاملات POST / GET وغير ذلك، ويمكنك استخدام أيّ آلية قاعدة بيانات تدعمها بيئة Node، إذ لا يحدد إطار عمل Express أيّ سلوك متعلق بقاعدة البيانات. سنشرح في الأقسام التالية بعض الأشياء التي ستراها عند العمل باستخدام شيفرة Express و Node. مثال لطباعة مرحبا بالعالم Helloworld باستخدام إطار عمل Express أولًا، ضع في بالك مثال مرحبًا بالعالم المعياري، إذ سنناقش كل جزء منه فيما يلي وفي الأقسام التالية. ملاحظة: إذا كان لديك بيئة Node وإطار عمل Express مُثبَّتين مسبقًا (أو إذا ثبّتهما كما هو موضح في المقال التالي)، فيمكنك حفظ الشيفرة البرمجية التالية في ملف نصي بالاسم "app.js" وتشغيله في موجه أوامر باش Bash من خلال استدعاء الأمر: node ./app.js. const express = require("express"); const app = express(); const port = 3000; app.get("/", function (req, res) { res.send("Hello World!"); }); app.listen(port, function () { console.log(`Example app listening on port ${port}!`); }); يطلب السطران الأوليان باستخدام require() الوحدةَ express وينشئان تطبيق Express. يحتوي هذا الكائن- الذي يُطلَق عليه تقليديًا الاسم app- على توابع لتوجيه طلبات HTTP وضبط البرمجيات الوسيطة وتصيير عروض HTML وتسجيل محرك القوالب وتعديل إعدادات التطبيق التي تتحكم في سلوك التطبيق، مثل وضع البيئة وما إذا كانت تعريفات الوجهة حساسة لحالة الأحرف وغير ذلك. يعرض الجزء الأوسط من الشيفرة (الأسطر الثلاثة التي تبدأ بالتابع app.get) تعريف الوجهة، إذ يحدّد التابع app.get() دالة رد النداء callback المُستدعاة عندما يكون هناك طلب HTTP من النوع GET له المسار (/) المتعلق بجذر الموقع. تأخذ دالة رد النداء طلبًا وكائن استجابة بوصفهما وسيطين، وتستدعي التابع send() للاستجابة لإعادة السلسلة النصية "Hello World!". تشغّل الكتلة النهائية الخادم على منفذ محدد ("3000") وتطبع تعليق سجل في الطرفية، ويمكنك مع تشغيل الخادم الانتقال إلى localhost:3000 في متصفحك لترى مثال الاستجابة المُعادة. استيراد وإنشاء الوحدات الوحدة Module هي مكتبة أو ملف جافا سكريبت يمكنك استيراده في شيفرة برمجية أخرى باستخدام دالة require() الخاصة ببيئة Node، إذ يُعَد إطار عمل Express بحد ذاته وحدةً مثل مكتبات البرمجيات الوسيطة وقواعد البيانات التي نستخدمها في تطبيقات Express. توضح الشيفرة البرمجية التالية كيفية استيراد وحدة باسمها باستخدام إطار عمل Express، إذ نستدعي أولًا الدالة require() من خلال تحديد اسم الوحدة بوصفها سلسلة نصية ('express')، ثم نستدعي الكائن المُعاد لإنشاء تطبيق Express، ويمكننا بعد ذلك الوصول إلى خاصيات ودوال كائن التطبيق. const express = require("express"); const app = express(); يمكنك أيضًا إنشاء وحداتك الخاصة التي يمكن استيرادها بالطريقة نفسها. ملاحظة: من فوائد إنشاء وحداتك الخاصة أنها تسمح لك بتنظيم شيفرتك البرمجية إلى أجزاء يمكن إدارتها، إذ يصعب فهم وصيانة تطبيق مؤلف من ملف واحد، ويساعدك استخدام الوحدات في إدارة فضاء أسمائك، إذ تُستورَد المتغيرات التي تصدّرها بصورة صريحة فقط عند استخدام إحدى الوحدات. يمكن جعل الكائنات متاحةً خارج الوحدة من خلال جعلها خاصيات إضافية في الكائن exports، فمثلًا تُعَد الوحدة square.js التالية ملفًا يصدّر التوابع area() و perimeter(): exports.area = function (width) { return width * width; }; exports.perimeter = function (width) { return 4 * width; }; يمكننا استيراد هذه الوحدة باستخدام الدالة require()، ثم استدعاء التابع (أو التوابع) المُصدَّرة كما يلي: const square = require("./square"); // نطلب هنا باستخدام الدالة require() اسم الملف بدون لاحقة الملف .js (الاختيارية) console.log(`The area of a square with a width of 4 is ${square.area(4)}`); ملاحظة: يمكنك أيضًا تحديد مسار مطلق، (أو اسم ما كما فعلنا في البداية) للوحدة. إذا أردتَ تصدير كائن كامل في مهمة واحدة بدلًا من بناء خاصيةً واحدة في كل مرة، فأسنده إلى module.exports كما يلي، ويمكنك إجراء ذلك لجعل جذر كائن exports بانيًا Constructor أو دالة أخرى: module.exports = { area(width) { return width * width; }, perimeter(width) { return 4 * width; }, }; ملاحظة: يمكنك عَدّ exports بوصفه اختصارًا للمصطلح module.exports ضمن وحدة معينة، ولكن يُعَد exports مجرد متغير مهيَّأ بقيمة module.exports قبل تقييم الوحدة، وهذه القيمة هي مرجع إلى كائن (كائن فارغ في هذه الحالة)، وهذا يعني أن exports يحتوي على مرجع للكائن نفسه الذي يشير إليه module.exports، مما يعني أيضًا أن exports لم يعد مرتبطًا بالكائن module.exports من خلال إسناد قيمة أخرى إلى exports. اطلع على مقال تعرف على وحدات Node.js الأساسية وإنشاء وحدات برمجية Modules في Node.js والوحدات Modules في توثيق Node لمزيد من المعلومات. استخدام واجهات برمجة التطبيقات غير المتزامنة تستخدم شيفرة جافا سكريبت في أغلب الأحيان واجهات برمجة تطبيقات غير متزامنة بدلًا من واجهات برمجة تطبيقات متزامنة للعمليات التي يمكن أن تستغرق بعض الوقت لتكتمل، حيث تُعَد واجهة برمجة التطبيقات المتزامنة واجهة يجب أن تكتمل فيها العملية قبل أن تبدأ العملية التي تليها مثل دوال log المتزامنة التالية التي ستطبع النص إلى الطرفية بالترتيب (First, Second): console.log("First"); console.log("Second"); بينما تبدأ واجهة برمجة التطبيقات غير المتزامنة عمليةً وتُعاد مباشرةً قبل اكتمال العملية، وستستخدم واجهة برمجة التطبيقات API بعض الآليات لإجراء عمليات إضافية بمجرد انتهاء العملية، فمثلًا ستطبع الشيفرة التالية "Second, First" لأنه العملية لا تكتمل إلا بعد عدة ثوانٍ بالرغم من استدعاء التابع setTimeout() أولًا والإعادة مباشرةً. setTimeout(function () { console.log("First"); }, 3000); console.log("Second"); يُعَد استخدام واجهات برمجة التطبيقات غير المتزامنة وغير المُعطِّلة في Node أكثر أهمية من استخدامها في المتصفح لأن بيئة Node هي بيئة تنفيذ ذات خيط برمجي Thread واحد وتقاد بالأحداث، فاعتمادها على خيط واحد يعني تشغيل جميع الطلبات الواردة إلى الخادم على خيط واحد (بدلًا من توليدها في عمليات منفصلة). يُعَد هذا النموذج فعالًا جدًا من حيث السرعة وموارد الخادم، ولكنه يعني أنه إذا استدعَت أيٌّ من دوالك توابعًا متزامنة تستغرق وقتًا طويلًا حتى تكتمل، فلن تعطِّل الطلب الحالي فحسب، بل ستعطِّل كل طلب آخر يعالجه تطبيقك. هناك عدد من الطرق لتُعلِم واجهةُ برمجة التطبيقات غير المتزامنة تطبيقَك بأنه اكتمل، ولكن الطريقة الأكثر شيوعًا هي تسجيل دالة رد النداء عند استدعاء واجهة برمجة التطبيقات غير المتزامنة، والتي ستُستدعَى مرة أخرى عند اكتمال العملية، وهذا هو الأسلوب الذي استخدمناه مسبقًا. ملاحظة: يمكن أن يكون استخدام دوال رد النداء "فوضويًا" إذا كان لديك سلسلة من العمليات غير المتزامنة الاعتمادية التي يجب إجراؤها بالترتيب لأن ذلك ينتج عنه مستويات متعددة من دوال رد النداء المتداخلة. تُعرف هذه المشكلة عمومًا باسم "جحيم دوال رد النداء Callback Hell"، ويمكن تقليل هذه المشكلة من خلال ممارسات كتابة الشيفرة البرمجية الجيدة باستخدام وحدةٍ مثل الوحدة async، أو إعادة إنتاج الشيفرة البرمجية إلى ميزات جافا سكريبت أصيلة مثل الوعود Promises واللاتزامن والانتظار async/await، حيث توفّر بيئة Node الدالة utils.promisify لتحويل دالة رد النداء إلى وعدٍ بطريقة مريحة. ملاحظة: من الأمور الشائعة لكلٍّ من Node و Express استخدام دوال رد النداء ذات قيمة الخطأ أولًا، حيث تكون القيمة الأولى في دوال رد النداء قيمة خطأ، بينما تحتوي الوسائط اللاحقة على بيانات نجاح. إنشاء معالجات الوجهة Route Handlers عرّفنا في مثال مرحبًا بالعالم باستخدام إطار عمل Express السابق دالة معالجة الوجهة (دالة رد لنداء) لطلبات HTTP من النوع GET إلى جذر الموقع ('/'). app.get("/", function (req, res) { res.send("Hello World!"); }); تأخذ دالة رد النداء كائني طلب واستجابة بوصفهما وسطاء، وتستدعي التابع send() في هذه الحالة للاستجابة لإعادة السلسلة النصية "Hello World!". هناك عدد من توابع الاستجابة الأخرى لإنهاء دورة الطلب/الاستجابة، فمثلًا يمكنك استدعاء التابع res.json() لإرسال استجابة بتنسيق JSON أو التابع res.sendFile() لإرسال ملف. ملاحظة: يمكنك استخدام أيّ أسماء وسطاء تريدها في دوال رد النداء، إذ سيكون المتغير الأول هو الطلب وسيظل الوسيط الثاني هو الاستجابة دائمًا عند استدعاء دالة رد النداء، ولكن من المنطقي تسميتها بحيث يمكنك تحديد الكائن الذي تعمل به في متن دالة رد النداء. يوفّر كائن تطبيق Express توابعًا لتعريف معالجات الوجهة لجميع أفعال HTTP الأخرى، والتي تُستخدَم غالبًا بالطريقة نفسها تمامًا وهي: checkout(), copy(), delete(), get(), head(), lock(), merge(), mkactivity(), mkcol(), move(), m-search(), notify(), options(), patch(), post(), purge(), put(), report(), search(), subscribe(), trace(), unlock(), unsubscribe() يوجد تابع توجيه خاص هو app.all() يُستدعَى في استجابة أيّ تابع HTTP، ويُستخدَم هذا التابع لتحميل دوال وسيطة في مسار معين لجميع توابع الطلب. يوضّح المثال التالي (من توثيق Express) معالجًا يُنفَّذ لطلبات القسم /secret بغض النظر عن فعل HTTP المُستخدَم (بشرط أن تدعمه وحدة http). app.all("/secret", function (req, res, next) { console.log("Accessing the secret section…"); next(); // تمرير التحكم إلى المعالج التالي }); تسمح الوجهات Routes بمطابقة أنماط معينة من المحارف في عنوان URL واستخراج بعض القيم من عنوان URL وتمريرها بوصفها معاملات إلى معالج الوجهة، مثل سمات كائن الطلب المُمرَّر بوصفه معاملًا. من المفيد تجميع معالجات الوجهة لجزء معين من الموقع مع بعضها بعضًا والوصول إليها باستخدام بادئة وجهة مشتركة، فمثلًا يحتوي موقعٌ له خاصية ويكي Wiki على جميع الوجهات المتعلقة بها في ملف واحد ويمكن الوصول إليها باستخدام بادئة وجهة هي "/wiki/"، ويمكن تحقيق ذلك في إطار عمل Express باستخدام كائن express.Router. يمكننا مثلًا إنشاء وجهة wiki في وحدة بالاسم wiki.js ثم تصدير كائن Router كما يلي: // wiki.js - وحدة وجهة Wiki const express = require("express"); const router = express.Router(); // وجهة الصفحة الرئيسية router.get("/", function (req, res) { res.send("Wiki home page"); }); // وجهة الصفحة About router.get("/about", function (req, res) { res.send("About this wiki"); }); module.exports = router; ملاحظة: تشبه إضافة الوجهات إلى الكائن Router إضافةَ الوجهات إلى الكائن app كما هو موضح سابقًا. سنطلب باستخدام الدالة require() وحدة الوجهة (wiki.js) لاستخدام الموجّه في ملف تطبيقنا الرئيسي، ثم نستدعي التابع use() في تطبيق Express لإضافة الكائن Router إلى مسار المعالجة الوسيطة، ويمكن بعد ذلك الوصول إلى الوجهتين من /wiki/ و /wiki/about/. const wiki = require("./wiki.js"); // … app.use("/wiki", wiki); اطلع على مقال الموجّهات والمتحكمات لمزيد من التفاصيل حول العمل مع الوجهات وخاصة استخدام الكائن Router. استخدام البرمجيات الوسيطة تُستخدَم البرمجيات الوسيطة على نطاق واسع في تطبيقات Express -للمهام بدءًا تخديم الملفات الثابتة إلى معالجة الأخطاء- لضغط استجابات HTTP. تنهي دوال الوجهات دورة طلب واستجابة HTTP من خلال إعادة الاستجابة لعميل HTTP، ولكن تجري الدوال الوسيطة بعض العمليات على الطلب أو الاستجابة ثم تستدعي الدالة التالية في المكدس stack، والتي يمكن أن تكون برمجية وسيطة أو معالج وجهة، ويعود ترتيب استدعاء البرمجيات الوسيطة إلى مطور التطبيق. ملاحظة: يمكن للبرمجيات الوسيطة إجراء أيّ عملية وتنفيذ أيّ شيفرة برمجية وإجراء تغييرات على كائن الطلب والاستجابة، ويمكنها إنهاء دورة الطلب والاستجابة. إذا لم تنتهِ الدورة، فيجب استدعاء التابع next() لتمرير التحكم إلى الدالة الوسيطة التالية، أو سيُترَك الطلب مُعلَّقًا. تستخدم معظم التطبيقات برمجيات وسيطة خارجية لتبسيط مهام تطوير الويب الشائعة، مثل العمل مع ملفات تعريف الارتباط cookies والجلسات واستيثاق المستخدمين والوصول إلى طلب POST وبيانات جسون JSON والتسجيل وغير ذلك. يمكنك العثور على قائمة بحزم البرمجيات الوسيطة التي يهتم بها فريق Express، والتي تتضمن أيضًا الحزم الخارجية الشائعة الأخرى، وتتوفر حزم Express الأخرى في مدير الحزم npm. يجب تثبيت برمجية وسيطة خارجية في تطبيقك باستخدام مدير الحزم npm لاستخدامها، فمثلًا يمكن تثبيت البرمجية الوسيطة morgan لتسجيل طلبات HTTP باستخدام الأمر التالي: npm install morgan يمكنك بعد ذلك استدعاء التابع use() في كائن تطبيق Express لإضافة البرمجيات الوسيطة إلى المكدس: const express = require("express"); const logger = require("morgan"); const app = express(); app.use(logger("dev")); // … ملاحظة: تُستدعَى دوال التوجيه والبرمجيات الوسيطة بالترتيب نفسه للتصريح عنها، إذ يكون الترتيب مهمًا بالنسبة لبعض البرمجيات الوسيطة، فمثلًا إذا اعتمدت البرمجيات الوسيطة للجلسة على البرمجيات الوسيطة لملفات تعريف الارتباط، فيجب إضافة معالج ملفات تعريف الارتباط أولًا. تُستدعَى البرمجيات الوسيطة قبل إعداد الوجهات غالبًا، أو لن تتمكن معالجات الوجهات من الوصول إلى الوظائف التي تضيفها برمجيتك الوسيطة. يمكنك كتابة دوالك الوسيطة، إذ يُحتمَل أن تضطر إلى ذلك إذا أردتَ إنشاء شيفرة معالجة الأخطاء فقط. يتمثل الاختلاف الوحيد بين دالة البرمجية الوسيطة ودالة رد استدعاء معالج الوجهة في أن دوال البرمجيات الوسيطة لها وسيط ثالث هو next الذي يُتوقَّع أن تستدعيه دوال البرمجيات الوسيطة إن لم تكن الدالة التي تكمل دورة الطلب، إذ تحتوي الدالة الوسيطة على الدالة next التي يجب استدعاؤها عند استدعاء هذه الدالة الوسيطة. يمكنك إضافة دالة وسيطة إلى سلسلة المعالجة لجميع الاستجابات باستخدام التابع app.use()، أو لفعل HTTP محدَّد باستخدام التابع المرتبط به مثل: app.get() و app.post() وغير ذلك، وتُحدَّد الوجهة بالطريقة نفسها لكلتا الحالتين، بالرغم من أن الوجهة اختيارية عند استدعاء التابع app.use(). يوضح المثال التالي كيفية إضافة الدالة الوسيطة باستخدام كلا الأسلوبين مع أو بدون وجهة: const express = require("express"); const app = express(); // مثال على دالة وسيطة const a_middleware_function = function (req, res, next) { // إجراء بعض العمليات next(); // استدعاء next() ليستدعي Express الدالة الوسيطة التالية في السلسلة }; // الدالة المُضافة باستخدام التابع use() لجميع الوجهات والأفعال app.use(a_middleware_function); // الدالة المُضافة باستخدام التابع use() لوجهة محددة app.use("/someroute", a_middleware_function); // الدالة الوسيطة المُضافة لفعل HTTP ووجهة محددة app.get("/", a_middleware_function); app.listen(3000); ملاحظة: صرّحنا عن دالة وسيطة بصورة منفصلة ثم ضبطناها بوصفها دالة رد نداء، وصرّحنا عن دالة رد النداء عند استخدامها في دالة معالج الوجهة السابقة، ويُعَد هذان الأسلوبان صالحين في جافا سكريبت. تخديم الملفات الثابتة يمكنك استخدام البرمجية الوسيطة express.static لتخديم الملفات الثابتة بما في ذلك الصور وملفات CSS وجافا سكريبت، وتُعَد static() الدالة الوسيطة الوحيدة التي هي جزء من إطار عمل Express. يمكنك مثلًا استخدام السطر التالي لتخديم الصور وملفات CSS وجافا سكريبت من مجلد بالاسم public في المستوى نفسه الذي تستدعيه فيه Node: app.use(express.static("public")); تُخدَّم الملفات في المجلد public من خلال إضافة اسم هذه الملفات (بالنسبة إلى المجلد "public" الأساسي) إلى عنوان URL الأساسي كما في الأمثلة التالية: http://localhost:3000/images/dog.jpg http://localhost:3000/css/style.css http://localhost:3000/js/app.js http://localhost:3000/about.html يمكنك استدعاء الدالة static() عدة مرات لتخديم مجلدات متعددة، بحيث إذا لم تعثر دالة وسيطة على ملفٍ ما، فسيُمرَّر إلى البرمجية الوسيطة التالية، إذ يعتمد ترتيب استدعاء البرمجيات الوسيطة على ترتيب تصريحها. app.use(express.static("public")); app.use(express.static("media")); يمكنك أيضًا إنشاء بادئة افتراضية لعناوين URL الثابتة الخاصة بك بدلًا من إضافة الملفات إلى عنوان URL الأساسي، فمثلًا نحدد فيما يلي مسار ربط Mount Path بحيث تُحمَّل الملفات باستخدام البادئة "/media": app.use("/media", express.static("public")); يمكنك الآن تحميل الملفات الموجودة في المجلد public من بادئة المسار /media: http://localhost:3000/media/images/dog.jpg http://localhost:3000/media/video/cat.mp4 http://localhost:3000/media/cry.mp3 معالجة الأخطاء تُعالَج الأخطاء باستخدام دالة وسيطة خاصة واحدة أو أكثر لها أربعة وسائط بدلًا من الوسائط الثلاثة المعتادة: (err, req, res, next) كما يلي: app.use(function (err, req, res, next) { console.error(err.stack); res.status(500).send("Something broke!"); }); تعيد الشيفرة السابقة أيّ محتوًى مطلوب، ولكن يجب استدعاؤها بعد جميع توابع app.use() واستدعاءات الوجهات الأخرى، بحيث تكون آخر برمجية وسيطة في عملية معالجة الطلب. يحتوي إطار عمل Express على معالج أخطاء مبني مسبقًا، والذي يهتم بأيّ أخطاء متبقية يمكن أن تواجهها في التطبيق، وتُضاف دالة معالجة الأخطاء الافتراضية هذه في نهاية مكدس الدوال الوسيطة. إذا مرّرتَ خطأً إلى الدالة next() ولم تعالجه في معالج الأخطاء، فسيعالجه معالج الأخطاء المبني مسبقًا، إذ سيُكتَب الخطأ إلى العميل باستخدام متعقّب المكدس stack trace. لا يُضمَّن متعقّب المكدس في بيئة الإنتاج، لذا يجب ضبط متغير البيئة NODE_ENV على القيمة 'production' لتشغيله في وضع الإنتاج. ملاحظة: لا يجري التعامل مع HTTP404 ورموز حالة الخطأ الأخرى بوصفها أخطاء، فإذا أردتَ معالجتها، فيمكنك إضافة دالة وسيطة لذلك. استخدام قواعد البيانات يمكن لتطبيقات Express استخدام أيّ آلية قاعدة بيانات تدعمها بيئة Node، ولا يعرّف Express أيّ سلوك أو متطلبات إضافية محددة لإدارة قاعدة البيانات، إذ هناك العديد من الخيارات لاستخدامها مثل PostgreSQL و MySQL و Redis و SQLite و MongoDB وغير ذلك. يجب أولًا تثبيت مشغّل قاعدة البيانات باستخدام مدير الحزم npm، فمثلًا يمكنك استخدام الأمر التالي لتثبيت مشغّل قاعدة بيانات NoSQL MongoDB: npm install mongodb يمكنك تثبيت قاعدة البيانات محليًا أو على خادم سحابي، إذ ستطلب المشغّل في شيفرة Express، ثم تتصل بقاعدة البيانات، ثم تجري عمليات الإنشاء والقراءة والتحديث والحذف -أو اختصارًا CRUD. يوضح المثال التالي (من توثيق Express) كيفية العثور على سجلات "mammal" باستخدام قاعدة بيانات MongoDB، وتعمل الشيفرة التالية مع الإصدارات الأقدم من إصدار mongodb الذي هو 2.2.33: const MongoClient = require("mongodb").MongoClient; MongoClient.connect("mongodb://localhost:27017/animals", (err, db) => { if (err) throw err; db.collection("mammals") .find() .toArray((err, result) => { if (err) throw err; console.log(result); }); }); يمكنك استخدام الشيفرة التالية بالنسبة للإصدار رقم 3.0 من mongodb والإصدارات الأحدث: const MongoClient = require("mongodb").MongoClient; MongoClient.connect("mongodb://localhost:27017/animals", (err, client) => { if (err) throw err; const db = client.db("animals"); db.collection("mammals") .find() .toArray((err, result) => { if (err) throw err; console.log(result); client.close(); }); }); هناك طريقة شائعة أخرى وهي الوصول إلى قاعدة بياناتك بطريقة غير مباشرة باستخدام رابط الكائنات العلائقي Object Relational Mapper -أو ORM اختصارًا، إذ تعرّف في هذه الطريقة بياناتك بوصفها كائنات أو نماذج ويربط رابط ORM هذه البيانات بتنسيق قاعدة البيانات الأساسية. تتمتع هذه الطريقة بفائدة أنه يمكنك -بصفتك مطورًا- الاستمرار في التفكير وفق مصطلحات كائنات جافا سكريبت بدلًا من التفكير وفق دلالات قاعدة البيانات، وأن هناك مكانًا واضحًا لإجراء التحقق من صحة البيانات الواردة وفحصها (سنتحدث أكثر عن قواعد البيانات في مقال لاحق (استخدام قاعدة البيانات). تصيير البيانات- العروض Views تسمح محركات القوالب (يُشار إليها أيضًا باسم "محركات العروض" في توثيق Express) بتحديد بنية مستندات الخرج في قالبٍ ما باستخدام العناصر البديلة للبيانات التي ستُملَأ عند توليد الصفحة، إذ تُستخدَم القوالب لإنشاء صفحات HTML، ولكن يمكنها أيضًا إنشاء أنواع أخرى من المستندات، ويدعم Express عددًا من محركات القوالب. تضبط في شيفرة إعدادات تطبيقك محركَ القوالب لاستخدامه وتضبط الموقع الذي يجب أن يبحث فيه Express عن القوالب باستخدام إعدادات 'views' و 'view engine' كما يلي، ويجب أيضًا تثبيت الحزمة التي تحتوي على مكتبة قوالبك: const express = require("express"); const path = require("path"); const app = express(); // اضبط المجلد ليتضمن القالب ('views') app.set("views", path.join(__dirname, "views")); // اضبط محرك القوالب للاستخدام، ويكون في هذه الحالة 'some_template_engine_name' app.set("view engine", "some_template_engine_name"); يعتمد مظهر القالب على المحرك الذي تستخدمه. بافتراض أن لديك ملف قالب بالاسم "index." الذي يحتوي على عناصر بديلة لمتغيرات البيانات المُسمَّاة "title" و "message"، فيمكنك استدعاء Response.render() في دالة معالج الوجهة لإنشاء استجابة HTML وإرسالها كما يلي: app.get("/", function (req, res) { res.render("index", { title: "About dogs", message: "Dogs rock!" }); }); بنية الملفات لا يضع إطار عمل Express أيّ افتراضات للبنية أو المكونات التي تستخدمها، إذ يمكن أن توضَع الوجهات والعروض والملفات الثابتة والشيفرة الأخرى الخاصة بالتطبيق في أيّ عدد من الملفات وبأيّ بنية مجلدات. يمكن أن يكون كامل تطبيق Express في ملف واحد، ولكن من المنطقي تقسيم تطبيقك إلى ملفات بناءً على الوظيفة، مثل إدارة الحسابات والمدونات ولوحات المناقشة، ونطاق المشكلات المعمارية، مثل النموذج، أو العرض، أو المتحكم إذا استخدمتَ معمارية MVC وهي اختصار لثلاث كلمات هي: النموذج Model الذي يعني بيانات التطبيق فهو يتفاعل مباشرة مع قاعدة البيانات الخاصة بك ويسترد المعلومات منها. العرض View الذي يعني واجهة التطبيق فهو يعرض الصفحات التي يتفاعل معها المستخدم مباشرة. المتحكم Controller وهو صلة الوصل بين العرض والنموذج فهو يستقبل طلبات المستخدمين ويسترد البيانات المطلوبة من النموذج ويعالجها ويرسلها إلى صفحات العرض. سنستخدم في مقال لاحق مولّد تطبيقات Express أو Express Application Generator الذي يُنشِئ تطبيقًا هيكليًا معياريًا يمكننا توسيعه بسهولة لإنشاء تطبيقات الويب. الخلاصة أكملنا الخطوة الأولى في رحلة تعلم Express/Node، إذ يجب أن تفهم الآن فوائد Express و Node الرئيسية، وما تبدو عليه الأجزاء الرئيسية لتطبيق Express (الوجهات والبرمجيات الوسيطة ومعالجة الأخطاء وشيفرة القوالب)، ويجب أن تفهم أيضًا أن الطريقة التي تجمع بها هذه الأجزاء مع بعضها بعضًا والمكتبات التي تستخدمها أمرٌ متروكٌ لك بسبب كون Express إطار عمل غير قائم على رأيه. يُعَد Express إطار عمل تطبيق ويب خفيف الوزن، إذ تأتي الكثير من فوائده وإمكاناته من مكتبات وميزات خارجية، إذ سنلقي نظرةً عليها بمزيد من التفصيل في المقالات التالية، وسنتعرّف في المقال التالي على إعداد بيئة تطوير Node، بحيث يمكنك البدء في رؤية بعض شيفرات Express. ترجمة -وبتصرُّف- للمقالين Express web framework (Node.js/JavaScript) و Express/Node introduction. اقرأ أيضًا المقال السابق: تعرف على أمان تطبيقات جانغو أطر عمل الويب من طرف الخادم دليل استخدام Node.js وإطار العمل Express للمبتدئين مدخل إلى Node.js وExpress