عبد الصمد العماري

الأعضاء
  • المساهمات

    24
  • تاريخ الانضمام

  • تاريخ آخر زيارة

السُّمعة بالموقع

1 Neutral
  1. إذا كنت تعمل على تطوير تطبيقك بنشاط، فإن استخدام Docker يمكنه تبسيط سير العمل وجعل عملية نشر التطبيق على الإنتاج سلسةً، إذ يمنح العمل بالحاويات في عمليّة التطوير العديد من المزايا، نذكر منها أنّ: البيئات متناسقة (consistent)، مما يعني أنه يمكنك اختيار اللغات والاعتماديات التي تريدها لمشروعك دون أن تقلق بشأن تعارضات النظام. البيئات معزولة (isolated)، ممّا يسهّل اكتشاف المشكلات وإدماج الأعضاء الجدد في الفريق. البيئات محمولة (portable)، ممّا يسمح لك بتحزيم ومشاركة شيفرتك مع الآخرين. يشرح لك هذا الدرس كيفية إعداد بيئة تطوير لتطبيق Node.js بالاعتماد على Docker. ستنشئ حاويتين، واحدة من أجل تطبيق Node والأخرى لقاعدة البيانات MongoDB، وذلك باستخدام Docker Compose. ونظرًا لأن هذا التطبيق يعمل على Node وMongoDB، فسيقوم برنامج الإعداد بما يلي: *مزامنة شيفرة التطبيق التي على المضيف مع الشيفرة التي في الحاوية لتسهيل التعديلات أثناء التطوير. التحقّق من أن التعديلات في شيفرة التطبيق تعمل دون الحاجة إلى إعادة تشغيل. إنشاء قاعدة بيانات محمية بمستخدم وكلمة مرور من أجل بيانات التطبيق. جعل هذه البيانات ثابتة ومستقرة (persistent). في نهاية هذا الدرس، سيكون لديك تطبيق لمعلومات سمك القرش يعمل بشكل جيّد على حاويات Docker: المتطلبات الأساسية لمتابعة هذا الدرس، ستحتاج إلى العناصر التالية: خادم تطوير يشتغل على نظام أوبونتو 18.04، إضافة إلى مستخدم غير جذري يمتلك صلاحيات sudo وجدار حماية نشط. للحصول على إرشادات حول كيفية إعدادها، يرجى الاطلاع على دليل إعداد الخادم الأولي. تثبيت Docker على خادمك، باتباع الخطوتين الأولى و الثانية لكيفية تثبيت واستخدام Docker على أوبونتو 18.04. تثبيت Compose Docker على خادمك، باتباع الخطوة الأولى من كيفية تثبيت Docker Compose على أوبونتو 18.04. الخطوة الأولى: استنساخ المشروع وتعديل الاعتماديات تتمثل الخطوة الأولى في إنشاء هذا الإعداد في استنساخ شيفرة المشروع وتعديل ملف package.json الخاص به، والذي يتضمن اعتماديات المشروع. سوف نضيف nodemon إلى اعتماديات المشروع devDependencies، مع تحديد أننا سنستخدمها أثناء التطوير. يضمن تشغيل التطبيق باستخدام nodemon أنه سيُعاد تشغيله تلقائيًا عند إجراء تغييرات على الشيفرة. انسخ، في البداية، مستودع nodejs-mongo-mongoose من حساب DigitalOcean Community GitHub. يتضمن هذا المستودع الشيفرة من الإعداد الموضح في كيفية دمج MongoDB مع تطبيقك Node، والذي يشرح كيفية دمج قاعدة بيانات MongoDB مع تطبيق Node موجودٍ سلفًا باستخدام Mongoose. انسخ المستودع في مجلّد يسمى node_project: git clone https://github.com/do-community/nodejs-mongo-mongoose.git node_project انتقل إلى المجلّد node_project: cd node_project افتح ملف package.json الخاص بالمشروع باستخدام nano أو المحرّر الذي تفضّله: nano package.json أسفل اعتماديات المشروع وفوق قوس الإغلاق، أنشئ كائن devDependencies جديد يتضمن nodemon: ... "dependencies": { "ejs": "^2.6.1", "express": "^4.16.4", "mongoose": "^5.4.10" }, "devDependencies": { "nodemon": "^1.18.10" } } احفظ الملف وأغلقه عند الانتهاء من التحرير. بوضعك لشيفرة المشروع في مكانها وتعديل اعتمادياته، يمكنك الانتقال إلى إعادة بناء الشيفرة من أجل سير عملٍ يعتمد على الحاويات. الخطوة الثانية: إعداد تطبيقك للعمل بالحاويات يعني تعديل التطبيق من أجل سير عملٍ يعتمد على الحاويات، جعل الشيفرة أكثر نمطيةً. إذ توفر الحاويات قابلية التنقل بين البيئات، ويجب أن تعكس الشيفرة ذلك من خلال البقاء قدر الإمكان منفصلةً عن نظام التشغيل الأساسي. لتحقيق ذلك، سنعيد تشكيل شيفرتنا للاستفادة بشكل أكبر من خاصية process.env الخاصة ب Node، والتي تعيد كائنًا يحتوي على معلومات حول بيئة المستخدم في وقت التشغيل. يمكننا استخدام هذا الكائن في شيفرتنا لتعيين معلومات التكوين ديناميكيًا في وقت التشغيل باستخدام متغيرات البيئة. لنبدأ بالملف app.js، الذي يمثّل نقطة دخول التطبيق الرئيسية لدينا. افتح هذا الملف: nano app.js سترى فيه تعريفًا للثابتة port، بالإضافة إلى الدالّة listen التي تستخدم هذه الثابتة لتحديد المنفذ الذي سيستمع إليه التطبيق: ... const port = 8080; ... app.listen(port, function () { console.log('Example app listening on port 8080!'); }); دعنا نعيد تعريف ثابتة port للسماح بالتعيين الديناميكي في وقت التشغيل باستخدام الكائن process.env. لذا، أجرِ التعديلات التالية على تعريف الثابتة وعلى الدالّة listen: ... const port = process.env.PORT || 8080; ... app.listen(port, function () { console.log(`Example app listening on ${port}!`); }); يعيّن تعريفنا الثابت الجديد المنفذ port ديناميكيًا باستخدام القيمة الممرّرة في وقت التشغيل أو القيمة 8080. وبالمثل، أعدنا كتابة الدّالة لاستخدام قالب نصّي حديث، والذي سيُقحم قيمة المنفذ عند الاستماع للاتصالات. ونظرًا لأننا سنعيّن منافذنا في أماكن أخرى، فستغنينا هذه المراجعات عن الاضطرار إلى مراجعة هذا الملف بشكل مستمر عند تغيير بيئتنا. احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد ذلك، سنعدّل معلومات اتصال قاعدة البيانات لحذف أي بيانات اعتماد في التكوينات. افتح الملف db.js الذي يحتوي على هذه المعلومات: nano db.js يقوم الملف حاليا بالأمور التالية: يستورد Mongoose، كائن تخطيط المستندات (ODM) الذي نستخدمه لإنشاء مخطّطات ونماذج لبيانات التطبيق. يعيّن بيانات اعتماد قاعدة البيانات كقيم ثابتة، بما في ذلك اسم المستخدم وكلمة المرور. يتصل بقاعدة البيانات باستخدام التابع mongoose.connect. لمزيد من المعلومات حول الملفّ، يرجى الاطلاع على الخطوة الثالثة من كيفية دمج MongoDB مع تطبيق Node الخاص بك. ستكون خطوتنا الأولى في تعديل الملف هي إعادة تحديد الثوابت التي تتضمن معلومات حساسة. تبدو هذه الثوابت حاليًا على النحو التالي: ... const MONGO_USERNAME = 'sammy'; const MONGO_PASSWORD = 'your_password'; const MONGO_HOSTNAME = '127.0.0.1'; const MONGO_PORT = '27017'; const MONGO_DB = 'sharkinfo'; ... يمكنك استخدام الكائن process.env لالتقاط قيم وقت التشغيل لهذه الثوابت بدلاً من الاضطرار لكتابة هذه المعلومات يدويًا. لذا عدّل هذا المقطع من الشيفرة لتبدو على هذا النحو: ... const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB } = process.env; ... احفظ الملف وأغلقه عند الانتهاء من التحرير. في هذه المرحلة، عدّلت الملف db.js للعمل بمتغيرات البيئة في تطبيقك، ولكنك مازلت تحتاج إلى وسيلة لتمرير هذه المتغيرات إلى تطبيقك. لننشئ ملف env. بقيمٍ يمكنك تمريرها إلى التطبيق في وقت التشغيل. افتح هذا الملف: nano .env سيتضمن هذا الملف المعلومات التي حذفتها من db.js وهي اسم المستخدم وكلمة المرور لقاعدة بيانات التطبيق، بالإضافة إلى إعداد المنفذ واسم قاعدة البيانات. لا تنسَ تحديث اسم المستخدم وكلمة المرور واسم قاعدة البيانات المدرجة هنا بمعلوماتك الخاصة: MONGO_USERNAME=sammy MONGO_PASSWORD=your_password MONGO_PORT=27017 MONGO_DB=sharkinfo لاحظ أننا حذفنا إعدادات المضيف التي كانت في الأصل في db.js. والآن، سنعرّف المضيف على مستوى الملف Docker Compose، إلى جانب معلومات أخرى حول الخدمات والحاويات. احفظ هذا الملف وأغلقه عندما تنتهي من التحرير. نظرًا لاحتواء الملف env. على معلومات حساسة، فستحتاج إلى الحرص على تضمينه في ملفات dockerignore. و gitignore. الخاصة بمشروعك حتى لا يُنسخ إلى وحدة إدارة الإصدار أو إلى حاوياتك. افتح ملفك dockerignore.: nano .dockerignore أضف السطر التالي إلى أسفل الملف: ... .gitignore .env احفظ الملف وأغلقه عند الانتهاء من التحرير. يحتوي الملف gitignore. الموجود في هذا المستودع على env. أصلًا، ولكن لا تتردد في التحقق من وجوده: nano .gitignore ... .env ... تمكنت في هذه المرحلة من استخراج المعلومات الحساسة من شيفرة المشروع واتخذت تدابير للتحكم في كيفية ومكان نسخ هذه المعلومات. يمكنك الآن العمل على تمتين الشيفرة الخاصّة بالاتصال بقاعدة البيانات بُغية تحسينها من أجل سير عملٍ يعتمد على الحاويات. الخطوة الثالثة: تعديل إعدادات اتصال قاعدة البيانات ستكون خطوتنا التالية هي جعل طريقة اتصال قاعدة البيانات أكثر متانة عن طريق إضافة شيفرة تعالج الحالات التي يفشل فيها تطبيقنا في الاتصال بقاعدة البيانات. ويُعدّ تقديم هذا المستوى من المرونة لشيفرة التطبيق ممارسةً ينصح بها عند التعامل مع الحاويات باستخدام "Compose". افتح الملفّ db.js لتحريره: nano db.js ستظهر لك الشفرة التي أضفناها سابقًا، إلى جانب ثابتة العنوان url الخاصة بالمعرّف URI لاتصال Mongo والتابع connect فيMongoose: ... const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB } = process.env; const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`; mongoose.connect(url, {useNewUrlParser: true}); يقبل التابع connect حاليًا خيارًا يطلب من Mongoose استخدام مفسّر URL الجديد الخاص بـMongo. دعنا نضيف بعض الخيارات الإضافية إلى هذا التابع لتعريف المعاملات (paramaters) لمحاولات إعادة الاتصال. يمكننا القيام بذلك عبر إنشاء ثابتة options تتضمّن المعلومات ذات الصلة، بالإضافة إلى خيار مفسّر عناوين URL الجديد. أسفل ثوابت Mongo، أضف التعريف التالي للثابتة options: ... const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB } = process.env; const options = { useNewUrlParser: true, reconnectTries: Number.MAX_VALUE, reconnectInterval: 500, connectTimeoutMS: 10000, }; ... يطلب الخيار reconnectTries من Mongoose مواصلة محاولة الاتصال إلى أجل غير محدّد، في حين تحدد reconnectInterval الفترة الفاصلة بين محاولتي اتصال بوحدة الجزء من الألف من الثانية. ويحدّد connectTimeoutMS الفترة التي سينتظرها برنامج التشغيل Mongo قبل إعلان فشل محاولة الاتصال في 10 ثوانٍ. يمكننا الآن استخدام الثابتة الجديدة options في التابع connect لضبط إعدادات اتصال Mongoose بدقّةٍ. سنضيف أيضًا كائن وعدٍ (promise) لمعالجة أخطاء الاتصال المحتملة. يبدو التابع connect الخاصّ ب Mongoose حاليًا على هذا النحو: ... mongoose.connect(url, {useNewUrlParser: true}); احذف التابع connect الموجود حاليًا وعوّضه بالشيفرة التالية التي تتضمن الثابتة options والوعد: ... mongoose.connect(url, options).then( function() { console.log('MongoDB is connected'); }) .catch( function(err) { console.log(err); }); في حالة اتصال ناجح، تسجّل دالّتنا رسالةً مناسبةً. وإلا فإنها تُمسك الخطأ وتسجّله، مما يتيح لنا استكشاف الأخطاء وإصلاحها. سيبدو الملف النهائي كما يلي: const mongoose = require('mongoose'); const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB } = process.env; const options = { useNewUrlParser: true, reconnectTries: Number.MAX_VALUE, reconnectInterval: 500, connectTimeoutMS: 10000, }; const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`; mongoose.connect(url, options).then( function() { console.log('MongoDB is connected'); }) .catch( function(err) { console.log(err); }); احفظ الملف وأغلقه عندما تنتهي من التحرير. لقد أضفت الآن مرونةً إلى شيفرة التطبيق لمعالجة الحالات التي قد يفشل فيها التطبيق في الاتصال بقاعدة البيانات. بوضعك لهذه الشيفرة في مكانها الصحيح، يمكنك الانتقال إلى تعريف خدماتك باستخدام "Compose". الخطوة الرابعة: تعريف الخدمات باستخدام Docker Compose بإعادة تشكيل الشيفرة، تكون مستعدًا لتحرير الملف docker-compose.yml بتعريفات خدماتك. الخدمة في Compose هي عبارة عن حاويةٍ قيد التشغيل، وتحتوي تعريفات الخدمة، التي ستتضمّنها في ملف docker-compose.yml، على معلومات حول كيفية اشتغال كل حاويةٍ صورةٍ (container image). تتيح لك الأداة "Compose" تعريف خدمات متعددة لإنشاء تطبيقات متعددة الحاويات. قبل تعريف خدماتنا، سنضيف، مع ذلك، أداةً إلى مشروعنا تسمى wait-for للتأكد من أن التطبيق لا يحاول الاتصال بقاعدة البيانات فقط بعد اكتمال مهام بدء تشغيل قاعدة البيانات. يستخدم هذا السكربت المجمِّع netcat لاستقصاء ما إذا كان المضيف والمنفذ المعيّنان يقبلان اتصالات TCP أم لا. ويتيح لك استخدامه التحكم في محاولات التطبيق للاتصال بقاعدة البيانات عن طريق اختبار جاهزية قاعدة البيانات لقبول الاتصالات من عدمها. رغم أنّ "Compose" يتيح لك تحديد الاعتماديات بين الخدمات باستخدام الخيار dependson، فإنّ هذا الترتيب يعتمد على ما إذا كانت الحاوية تعمل أم لا وليس على جاهزيتها. لن يكون استخدام dependson هو الأمثل لإعدادنا، لأننا نريد أن يتّصل تطبيقنا بقاعدة البيانات فقط عند اكتمال مهام بدء التشغيل فيها، بما في ذلك إضافة مستخدم وكلمة مرور إلى استيثاق قاعدة البيانات admin. لمزيد من المعلومات حول استخدام wait-for والأدوات الأخرى للتحكم في ترتيب بدء التشغيل، يرجى الاطلاع على التوصيات ذات الصلة في توثيق Compose. افتح ملفًا يسمى wait-for.sh: nano wait-for.sh انسخ الشيفرة التالية في هذا الملف لإنشاء دالة الاستقصاء: #!/bin/sh # original script: https://github.com/eficode/wait-for/blob/master/wait-for TIMEOUT=15 QUIET=0 echoerr() { if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi } usage() { exitcode="$1" cat << USAGE >&2 Usage: $cmdname host:port [-t timeout] [-- command args] -q | --quiet Do not output any status messages -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout -- COMMAND ARGS Execute command with args after the test finishes USAGE exit "$exitcode" } wait_for() { for i in `seq $TIMEOUT` ; do nc -z "$HOST" "$PORT" > /dev/null 2>&1 result=$? if [ $result -eq 0 ] ; then if [ $# -gt 0 ] ; then exec "$@" fi exit 0 fi sleep 1 done echo "Operation timed out" >&2 exit 1 } while [ $# -gt 0 ] do case "$1" in *:* ) HOST=$(printf "%s\n" "$1"| cut -d : -f 1) PORT=$(printf "%s\n" "$1"| cut -d : -f 2) shift 1 ;; -q | --quiet) QUIET=1 shift 1 ;; -t) TIMEOUT="$2" if [ "$TIMEOUT" = "" ]; then break; fi shift 2 ;; --timeout=*) TIMEOUT="${1#*=}" shift 1 ;; --) shift break ;; --help) usage 0 ;; *) echoerr "Unknown argument: $1" usage 1 ;; esac done if [ "$HOST" = "" -o "$PORT" = "" ]; then echoerr "Error: you need to provide a host and port to test." usage 2 fi wait_for "$@" احفظ الملف وأغلقه عند الانتهاء من إضافة الشيفرة. اجعل السكربتَ قابلًا للتنفيذ: chmod +x wait-for.sh بعد ذلك، افتح الملف docker-compose.yml: nano docker-compose.yml ابدأ بتعريف خدمة تطبيق nodejs عبر إضافة الشيفرة التالية إلى الملف: version: '3' services: nodejs: build: context: . dockerfile: Dockerfile image: nodejs container_name: nodejs restart: unless-stopped env_file: .env environment: - MONGO_USERNAME=$MONGO_USERNAME - MONGO_PASSWORD=$MONGO_PASSWORD - MONGO_HOSTNAME=db - MONGO_PORT=$MONGO_PORT - MONGO_DB=$MONGO_DB ports: - "80:8080" volumes: - .:/home/node/app - node_modules:/home/node/app/node_modules networks: - app-network command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js يتضمّن تعريف خدمة nodejs الخيارات التالية: build: هذا يحدد خيارات التكوين، بما في ذلك context وdockerfile، والتي ستُطبّق عندما يبني Docker صورة التطبيق. إذا كنت ترغب في استخدام صورة موجودة من سجلٍّ مثل Docker Hub، فيمكنك استخدام التعليمة image بدلاً من ذلك، مع معلومات حول اسم المستخدم والمستودع ووسم الصورة. context: هذا يحدّد سياق البناء الخاص ببناء الصورة، وهو في هذه الحالة مجلّد المشروع الحالي. dockerfile : هذا يحدّد الملفّ Dockerfile في مجلّد المشروع الحالي لكي يستخدم Compose هذا الملفّ لبناء صورة التطبيق. لمزيد من المعلومات حول هذا الملف، يرجى الاطلاع على [كيفية إنشاء تطبيق Node.js باستخدام Docker](رابط المقال الأول في هذه السلسلة). image و container_name: لإعطاء اسم لكلّ من الصورة والحاوية. restart: هذا يحدّد سياسة إعادة تشغيل الحاوية. تكون القيمة الافتراضية هي no، لكننا أعدنا تعيينها في yes ما لم يتم إيقافها. env_file: هذا يطلب من Compose إضافة متغيرات البيئة من ملفٍّ يسمى env. موجود في سياق البناء. environment: يتيح لك استخدام هذا الخيار إضافة إعدادات اتصال Mongo التي حدّدتها في ملف env. لاحظ أننا لا نعيّن NODEENV على development، لأن هذا هو السلوك الافتراضي لـ Express إذا لم يتم تعيين NODEENV. عند الانتقال إلى الإنتاج، يمكنك تعيين هذا على production لإتاحة التخزين المؤقت للعرض والتقليل من رسائل الخطأ المطوّلة. لاحظ أيضًا أننا حددنا حاوية قاعدة البيانات db كمضيف، كما وضّحناه في الخطوة الثانية. port: هذا يوجّه المنفذ 80 على المضيف إلى المنفذ 8080 على الحاوية. Volumes: نضمّن هنا نوعين من الربط: الأول هو وصل ترابطي (bind mount) يحمّل شيفرة التطبيق على المضيف إلى المجلّد /home/node/app في الحاوية. سيسهل هذا الأمر التطوير بسرعة، إذ سيتم إدخال أي تغييرات تجريها على شيفرة المضيف في الحاوية على الفور. والثاني هو عبارة عن حجمٍ مُسمّى nodemodules. عندما ينفّذ Docker التعليمة npm install المدرجة في Dockerfile، سينشئ npm مجلّدًا جديدًا nodemodules على الحاوية يتضمن الحزم المطلوبة لتشغيل التطبيق. ومع ذلك، سيحجب الوصل الترابطي الذي أنشأناه المجلّدَ nodemodules حديث الإنشاء. ونظرًا لأن nodemodules على المضيف فارغٌ، فسيعمل الوصل على تعيين مجلّدٍ فارغ على الحاوية، مع تحييد المجلّد nodemodules الجديد ومنع التطبيق من بدء التشغيل. ويعمل الحجم nodemodules المسمى على حلّ هذه المشكلة من خلال تثبيت محتويات المجلّد /home/node/app/node_modules وربطه مع الحاوية، مع إخفاء الربط. ضع في حسبانك النقاط التالية عند استخدام هذه الطريقة: سيعمل الربط على تحميل محتويات مجلّد node_modules على الحاوية إلى المضيف وسيكون هذا المجلّد مِلكًا للجذر root، مادام الحجم المسمى أنشئ بواسطة Docker. إذا كان لديك مجلّد nodemodules موجود مسبقًا على المضيف، فسيحيِّد المجلّد nodemodules الذي أنشئ على الحاوية. يفترض الإعداد الذي نبنيه في هذا الدرس أنك لا تملك مجلّدًا node_modules موجود مسبقًا وأنك لن تعمل مع npm على مضيفك. هذا يتماشى مع طريقة اثني عشر عامل لتطوير التطبيقات التي تقلل من الاعتماديات بين بيئات التنفيذ. networks: هذا يحدّد أن خدمة التطبيق ستنضم إلى الشبكة app-network التي سنعرّفها في أسفل الملف. command: يتيح لك هذا الخيار تعيين الأمر الذي يجب تنفيذه عندما يشغّل Compose الصورة. لاحظ أن هذا سوف يحيّد تعليمات CMD التي وضعناها في تطبيق Dockerfile الخاص بنا. نشغِّل هنا التطبيق باستخدام السكربت wait-for، الذي سيستقصي الخدمة db على المنفذ 27017 لاختبار جاهزية خدمة قاعدة البيانات من عدمها. بمجرد نجاح اختبار الاستعداد، سينفذ السكربت الأمر الذي حددناه، ‎/home/node/app/node_modules/.bin/nodemon app.js، لتشغيل التطبيق عبر nodemon. وسيضمن هذا إعادة تحميل أي تعديلات مستقبلية نجريها على شيفرتنا دون الحاجة إلى إعادة تشغيل التطبيق. بعد ذلك، أنشئ خدمة db من خلال إضافة الشيفرة التالية أسفل تعريف خدمة التطبيق: الملف ‎~/node_project/docker-compose.yml: ... db: image: mongo:4.1.8-xenial container_name: db restart: unless-stopped env_file: .env environment: - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD volumes: - dbdata:/data/db networks: - app-network تظل بعض الإعدادات التي حددناها لخدمة nodejs كما هي، لكننا أجرينا أيضًا التعديلات التالية على تعريفات image و environment و volumes: image: لإنشاء هذه الخدمة، سيسحب Compose صورة 4.1.8-xenial الخاصة ب Mongo من Docker Hub. نحن نثبّت هنا إصدارًا معينًا لتجنب التعارضات المستقبلية المحتملة عند تغير صورة Mongo. لمزيد من المعلومات حول تثبيت الإصدار، يرجى الاطلاع على توثيق Docker حول أفضل ممارسات Dockerfile. MONGOINITDBROOTUSERNAME و MONGOINITDBROOTPASSWORD: تجعل صورة mongo متغيرات البيئة هذه متاحةً لكي تستطيع تعديل التهيئة الأولية لمثيل قاعدة بياناتك. وينشئ MONGOINITDBROOTUSERNAME و MONGOINITDBROOTPASSWORD معًا مستخدمًا root في استيثاق admin لقاعدة البيانات مع التأكد من تفعيل الاستيثاق عند بدء تشغيل الحاوية. لقد عيّننا MONGOINITDBROOTUSERNAME و MONGOINITDBROOTPASSWORD باستخدام قيم مأخوذة من الملف env.، والتي نمرّرها إلى الخدمة db عبر الخيار env_file. يعني القيام بذلك أن مستخدم التطبيق sammy سيكون مستخدمًا أساسيًا في مثيل قاعدة البيانات، مع إمكانية الوصول إلى جميع الصلاحيات الإدارية والتشغيلية لهذا الدور. عند العمل على الإنتاج، سترغب في إنشاء مستخدم تطبيق مخصص ذي صلاحيات محدّدة بدقّة. dbdata:/data/db: سيثبّت الحجم المسمى dbdata البيانات المخزنة في مجلّد البيانات الافتراضي ‎/data/db الخاص بـ Mongo . سيضمن هذا أنك لن تفقد البيانات في الحالات التي توقف فيها أو تحذف الحاويات. لقد أضفنا أيضًا خدمة db إلى شبكة app-network باستخدام الخيار networks. كخطوة أخيرة، أضف تعريفات الحجم والشبكة إلى أسفل الملف: ... networks: app-network: driver: bridge volumes: dbdata: node_modules: تتيح شبكة المستخدم الجسرية app-network التواصل بين حاوياتنا مادامت موجودة على نفس المضيف العفريت Docker. يعمل هذا على تبسيط حركة المرور والاتصال داخل التطبيق، إذ يفتح جميع المنافذ بين الحاويات على نفس الشبكة الجسرية، دون تعريض أي منفذ للعالم الخارجي. وبالتالي، فإن حاويات db و nodejs تستطيع التواصل مع بعضها البعض، وسنحتاج فقط إلى فتح المنفذ 80 للوصول الأمامي (front-end access) إلى التطبيق. يعرّف مفتاح المستوى الأعلى volumes الحجمين dbdata و node_modules. وعندما ينشئ Docker حجومًا، تُخزّن محتوياتها في جزء من ملفات المضيف النظامية، /var/lib/docker/volumes/ ، التي يديرها Docker. تخزّن محتويات كل حجمٍ في مجلّد تحت /var/lib/docker/volumes/ وتُثبت على أي حاوية تستخدم الحجم. وبهذه الطريقة، ستكون بيانات معلومات سمك القرش التي سيقوم المستخدمون بإنشائها مثبّتة في الحجم dbdata حتى لو أزلنا الحاوية db وأعدنا إنشاءها. سيبدو ملف docker-compose.yml النهائي كما يلي: version: '3' services: nodejs: build: context: . dockerfile: Dockerfile image: nodejs container_name: nodejs restart: unless-stopped env_file: .env environment: - MONGO_USERNAME=$MONGO_USERNAME - MONGO_PASSWORD=$MONGO_PASSWORD - MONGO_HOSTNAME=db - MONGO_PORT=$MONGO_PORT - MONGO_DB=$MONGO_DB ports: - "80:8080" volumes: - .:/home/node/app - node_modules:/home/node/app/node_modules networks: - app-network command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js db: image: mongo:4.1.8-xenial container_name: db restart: unless-stopped env_file: .env environment: - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD volumes: - dbdata:/data/db networks: - app-network networks: app-network: driver: bridge volumes: dbdata: node_modules: احفظ الملف وأغلقه عند الانتهاء من التحرير. بوضعك لتعريفات الخدمة في مكانها الصحيح، تكون مستعدًّا لبدء تشغيل التطبيق. الخطوة الخامسة: اختبار التطبيق بعد إنشاء وتحرير ملف docker-compose.yml ، يمكنك إنشاء خدماتك باستخدام الأمر docker-compose up. يمكنك أيضًا اختبار ثبات بياناتك عبر إيقاف حاوياتك وحذفها باستخدام Docker. ابدأ أولًا ببناء صور الحاوية وإنشاء الخدمات عبر تنفيذ docker-compose up باستخدام الراية d- ، والذي سيشغّل حاويات nodejs و db في الخلفية: docker-compose up -d سيظهر لك الإخراج الذي يؤكد أن خدماتك أُنشئت فعلًا: ... Creating db ... done Creating nodejs ... done يمكنك أيضًا الحصول على معلومات أكثر تفصيلاً حول عمليات التشغيل بعرضك لإخراج سجل الخدمات: docker-compose logs سيظهر لك إخراج مثل هذا إذا اشتغل كل شيء بشكل صحيح: ... nodejs | [nodemon] starting `node app.js` nodejs | Example app listening on 8080! nodejs | MongoDB is connected ... db | 2019-02-22T17:26:27.329+0000 I ACCESS [conn2] Successfully authenticated as principal sammy on admin يمكنك أيضًا التحقق من حالة حاوياتك باستخدام docker-compose ps: docker-compose ps سيظهر لك الإخراج التالي مشيرًا إلى أن حاوياتك تعمل: Name Command State Ports ---------------------------------------------------------------------- db docker-entrypoint.sh mongod Up 27017/tcp nodejs ./wait-for.sh db:27017 -- ... Up 0.0.0.0:80->8080/tcp بعد تشغيل خدماتك، يمكنك تصفّح: http://yourserverip في المتصفح. ستظهر لك صفحة الهبوط على هذا النحو: انقر على زر الحصول على معلومات القرش. ستظهر لك صفحة بنموذج إدخال يمكنك إدخال الاسم ووصف السلوك العام لسمك القرش: أضف سمكة قرش من اختيارك في النموذج. لأغراض هذا العرض التوضيحي، سنضيف Megalodon Shark إلى حقل Shark Name، وAncient إلى حقل Shark Character: انقر على زر الإرسال. ستظهر لك صفحة بها معلومات القرش معروضة: كخطوة أخيرة، يمكننا اختبار ثبات البيانات التي أدخلتها للتو إذا حذفنا حاوية قاعدة البيانات. ارجع مرة أخرى إلى الطرفية، واكتب الأمر التالي لإيقاف وحذف الحاويات والشبكة: docker-compose down لاحظ أننا لم ندرج الخيار ‎--volumes؛ وبالتالي، لن يُحذف الحجم dbdata. يؤكد الإخراج التالي حذف الحاويات والشبكة: Stopping nodejs ... done Stopping db ... done Removing nodejs ... done Removing db ... done Removing network node_project_app-network أعد الآن إنشاء الحاويات: docker-compose up -d ثم عد إلى نموذج معلومات سمك القرش: أدخل سمكة قرش جديدة من اختيارك. سنستعمل هنا Whale Shark وLarge: بمجرد النقر على زر الإرسال، سترى أن القرش الجديد أُضيف إلى مجموعة سمك القرش في قاعدة بياناتك دون أن تفقد البيانات التي أدخلتها سابقًا: إنّ تطبيقك يشتغل الآن على حاويات Docker مع تفعيل خاصيتي ثبات البيانات ومزامنة الشيفرة. خاتمة باتباع هذا الدرس، أنشأت إعداد تطويرٍ لتطبيقك Node باستخدام حاويات Docker. لقد تمكنت من جعل مشروعك أكثر نمطيّةً وأكثر قابلية للنقل من خلال استخراج المعلومات الحساسة وفصل حالة التطبيق عن شيفرة التطبيق. لقد أعددت أيضًا تكوين ملف الشيفرة المتداولة docker-compose.yml الذي تستطيع مراجعته كلما تغيرت احتياجاتك ومتطلبات التطوير الخاصة بك. قد تكون مهتمًا، أثناء عمليتك التطويرية ، بمعرفة المزيد حول تصميم التطبيقات بسير عمل يعتمد على الحاويات وعلى Cloud Native. يرجى الاطلاع على تصميم التطبيقات لKubernetes وتحديث التطبيقات ل Kubernetes إذا كانت لغتك الإنجليزية جيدة لمزيد من المعلومات حول هذه المواضيع. لمعرفة المزيد حول الشيفرة المستخدم في هذا الدرس، يرجى الاطلاع على كيفية إنشاء تطبيق Node.js باستخدام Docker وكيفية دمج MongoDB مع تطبيق Node الخاص بك. وللحصول على معلومات حول نشر تطبيق Node باستخدام وكيل عكسي Nginx يعتمد على الحاويات، يرجى الاطلاع على كيفية تأمين تطبيق Node.js في حاويات باستخدام Nginx و Let’s Encrypt و Docker Compose. ترجمة -وبتصرف- للمقال Containerizing a Node.js Application for Development With Docker Compose لصاحبته Kathleen Juell
  2. قد تجد نفسك، وأنت تستخدم node.js، أنك بصدد تطوير مشروع يُخزّن البيانات والاستعلام عنها. في هذه الحالة، ستحتاج إلى اختيار حلّ تقني لقاعدة البيانات يكون ملائما لأنواع البيانات والاستعلامات التي يستخدمها تطبيقك. في هذا الدّرس، ستعمل على دمج قاعدة بيانات MongoDB مع تطبيق Node موجودٍ سلفًا. يمكن أن تكون قواعد البيانات NoSQL مثل MongoDB مفيدةً إذا كانت متطلبات بياناتك تتضمن قابلية التوسع والمرونة. ويتكامل MongoDB جيدًا مع Node لأنه مصمم للعمل بشكل غير متزامن مع كائنات JSON. من أجل دمج MongoDB في مشروعك، سوف تستخدم Object Document Mapper) ODM) الخاص ب Mongoose لإنشاء مخططات ونماذج بيانات لتطبيقك. سيتيح لك ذلك تنظيم شيفرة التطبيق وفقًا للنمط الهيكلي MVC، والذي يسمح بفصل المنطق الذي يحكم كيفية معالجة التطبيق لإدخالات المستخدم عن ذلك الذي يتعلق بكيفية هيكلة البيانات وتقديمها إلى المستخدم. ويسهّل الاعتمادُ على هذا النمط الاختبار والتطوير في المستقبل عبر فصل المتعلّقات في قاعدة البيانات الخاصة بك. في نهاية البرنامج التعليمي، سيكون لديك تطبيق جاهز خاصّ بمعلومات سمك القرش يعمل على أخذ مدخلات المستخدم حول أسماك القرش المفضلة لديه وعرض النتائج في المتصفح: المتطلبات الأساسية جهاز أو خادم تطوير محلي يعمل على نظام أوبونتو 18.04، إلى جانب مستخدم غير جذري بصلاحيات sudo وجدار حماية نشط. للحصول على إرشادات حول كيفية إعدادها على خادم 18.04، يرجى الاطلاع على دليل إعداد الخادم الأولي. Node.js و npm مثبتين على الجهاز أو الخادم، باتباع هذه الإرشادات حول التثبيت باستخدام PPA المدار بواسطة NodeSource. تثبيت MongoDB على الجهاز أو الخادم، باتباع الخطوة الأولى من كيفية تثبيت MongoDB في أوبونتو 18.04. الخطوة الأولى: إنشاء مستخدم Mongo قبل أن نبدأ العمل بشيفرة التطبيق، سننشئ مستخدمًا إداريًا يملك صلاحية الوصول إلى قاعدة بيانات التطبيق. سيكون لهذا المستخدم أذونات إدارية على أي قاعدة بيانات، مما يمنحك المرونة في التبديل وإنشاء قواعد بيانات جديدة حسب الحاجة. أولاً، تحقق أن MongoDB يشتغل على خادمك: sudo systemctl status mongodb يدلّ الإخراج التالي على أنّ MongoDB يشتغل: ● mongodb.service - An object/document-oriented database Loaded: loaded (/lib/systemd/system/mongodb.service; enabled; vendor preset: enabled) Active: active (running) since Thu 2019-01-31 21:07:25 UTC; 21min ago ... بعد ذلك، افتح الصدفة الخاصة ب Mongo من أجل إنشاء مستخدمك: Mongo سينقلك هذا الأمر إلى صدفة إدارية: MongoDB shell version v3.6.3 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.6.3 ... > ستظهر لك بعض التحذيرات الإدارية عند فتح الصدفة (shell) نظرًا لغياب قيود على وصولك إلى قاعدة بيانات الخاصة بالمشرف admin. لمعرفة المزيد حول تقييد هذا الوصول يمكنك قراءة كيفية تثبيت وتأمين MongoDB على أوبونتو 18.04، عندما تنتقل إلى إعدادات الإنتاج. تستطيع الآن الاستفادة من وصولك إلى قاعدة بيانات المشرف admin لإنشاء مستخدم يمتلك الأذونات userAdminAnyDatabase التي تتيح الوصول المحمي بكلمة مرور لقواعد بيانات التطبيق. حدّد في الصّدفة shell، أنك تريد استخدام قاعدة بيانات المشرف admin لإنشاء مستخدمك: use admin بعد ذلك، أنشئ دورًا (role) وكلمة مرور عبر إضافة اسم مستخدم وكلمة مرور باستخدام الأمر db.createUser. بعد كتابة هذا الأمر، ستُظهِر الصدفة ثلاث نقاط قبل كل سطر حتى يكتمل تنفيذ الأمر. تأكد من تغيير المستخدم وكلمة المرور المقدمين هنا باسم المستخدم وكلمة المرور الخاصين بك: db.createUser( { user: "sammy", pwd: "your_password", roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] } ) سينشئ هذا الأمر إدخالًا للمستخدم sammy في قاعدة بيانات المشرف admin. وسيعمل اسم المستخدم الذي تحدده وقاعدة بيانات المشرف admin كمحدّدات للمستخدم. وسيبدو إخراج العملية برمّتها كما يلي، بما في ذلك الرسالة التي تشير إلى نجاح الإدخال: > db.createUser( ... { ... user: "sammy", ... pwd: "your_password", ... roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] ... } ...) Successfully added user: { "user" : "sammy", "roles" : [ { "role" : "userAdminAnyDatabase", "db" : "admin" } ] } يمكنك بعد إنشاء المستخدم وكلمة المرور، الخروج من الصّدفة الخاصة ب Mongo: exit وتستطيع كذلك الآن، بعد أن أنشأت مستخدم قاعدة البيانات الخاصة بك، الانتقال إلى استنساخ شيفرة المشروع البدئية وإضافة مكتبة Mongoose، والتي سوف تسمح لك بتنفيذ المخططات والنماذج للمجموعات (collections) في قواعد بياناتك. الخطوة الثانية: إضافة معلومات Mongoose وقاعدة البيانات إلى المشروع ستكون خطوتنا التالية هي استنساخ شيفرة التطبيق البدئية وإضافة معلومات Mongoose وقاعدة بيانات MongoDB إلى المشروع. في المجلّد الرئيسي للمستخدم غير الجذري، استنسخ مستودع nodejs-image-demo من حساب DigitalOcean Community GitHub. ويتضمن مستودع التخزين هذا الشيفرة الخاصة بالإعداد الموضح في كيفية إنشاء تطبيق Node.js باستخدام Docker. انسخ المستودع في مجلّد يسمى node_project: git clone https://github.com/do-community/nodejs-image-demo.git node_project انتقل إلى المجلّد node_project: cd node_project قبل تعديل شيفرة المشروع، دعنا نلقي نظرة على بنية المشروع باستخدام الأمر tree . ملحوظة: يعدّ الأمر tree مفيدًا لعرض بنيات الملفات والمجلّدات من سطر الأوامر. يمكنك تثبيته باستخدام الأمر التالي: sudo apt install tree ومن أجل استخدامه، انتقل بالأمر cd إلى مجلّد معين واكتب tree. يمكنك أيضًا أن تزوّد الأمر بكامل المسار إلى النقطة المحدّدة باستخدام أمر مثل: tree /home/sammy/sammys-project اكتب ما يلي لمعاينة المجلّد node_project: tree تبدو بنية المشروع الحالي كما يلي: ├── Dockerfile ├── README.md ├── app.js ├── package-lock.json ├── package.json └── views ├── css │ └── styles.css ├── index.html └── sharks.html سنضيف كلما تقدمنا في هذا البرنامج التعليمي بعض المجلّدات إلى هذا المشروع، وسيكون الأمر tree مفيدًا لمساعدتنا في تتبع تقدمنا. بعد ذلك، أضف حزمة npm mongoose إلى المشروع باستخدام الأمر npm install: npm install mongoose سينشئ هذا الأمر مجلّد node_modules في مجلّد مشروعك، باستخدام الاعتماديات المدرجة في ملف package.json الخاص بالمشروع، وسيضيف كذلك mongoose إلى هذا المجلّد. سيضيف mongoose أيضًا إلى الاعتماديات المدرجة في ملفك package.json. للحصول على توضيح أكثر تفصيلا عن package.json، يرجى الاطلاع على الخطوة الأولى في كيفية إنشاء تطبيق Node.js باستخدام Docker. قبل إنشاء أي مخططات أو نماذج لMongoose، سنضيف معلومات اتصال قاعدة البيانات حتى يتمكن التطبيق من الاتصال بقاعدة البيانات الخاصة بنا. من أجل فصل العناصر المتعلّقة بالتطبيق قدر الإمكان، أنشئ ملفًا منفصلًا لمعلومات اتصال قاعدة البيانات باسم db.js. ويمكنك فتح هذا الملف باستخدام nano أو المحرر المفضل لديك: nano db.js ستستورد أولاً وحدة mongoose باستخدام الدالّة require: الملف ‎~/node_project/db.js: const mongoose = require('mongoose'); سوف يتيح لك ذلك الوصول إلى توابع Mongoose المدمجة، والتي ستستخدمها لإنشاء اتصال بقاعدة البيانات الخاصة بك. بعد ذلك، أضف الثوابت التالية لتحديد معلومات الاتصال URI الخاصة بMongo. رغم أن اسم المستخدم وكلمة المرور اختياريان، ولكننا سندرجهما حتى نتمكن من طلب مصادقة الهوية على قاعدة البيانات. تأكد من استبدال اسم المستخدم وكلمة المرور بمعلوماتك الخاصة، وتبقى لك الحرية في طلب شيء آخر من قاعدة البيانات غير sharkinfo إذا كنت تفضل ذلك: الملف ‎~/node_project/db.js: const mongoose = require('mongoose'); const MONGO_USERNAME = 'sammy'; const MONGO_PASSWORD = 'your_password'; const MONGO_HOSTNAME = '127.0.0.1'; const MONGO_PORT = '27017'; const MONGO_DB = 'sharkinfo'; نظرًا لأننا نشغّل قاعدة البيانات محليًا، فقد استخدمنا 127.0.0.1 اسمًا للمضيف. قد يكون الأمر مغايرًا في سياقات تطوير أخرى: على سبيل المثال، إذا كنت تستخدم خادم قاعدة بيانات منفصل أو تعمل باستخدام عقد متعدّدة في إطار سير عمل يتضمن حاويات. ختامًا، حدّد قيمة ثابتة لـ URI وأنشئ الاتصال باستخدام التابع ()mongoose.connect: الملف ‎~/node_project/db.js: ... const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`; mongoose.connect(url, {useNewUrlParser: true}); لاحظ أننا في URI حدّدنا authSource لمستخدمنا كمشرف لقاعدة البيانات. ويعدّ هذا الأمر ضروريًا لأننا حددنا اسم مستخدم في سلسلة الاتصال connection string. ويحدّد استخدام الراية useNewUrlParser مع التابع ()mongoose.connect أننا نريد استخدام مفسّر URL الجديد الخاص بـ Mongo. احفظ الملف وأغلقه عند الانتهاء من التحرير. كخطوة أخيرة، أضف معلومات اتصال قاعدة البيانات إلى ملف app.js بحيث يمكن للتطبيق استخدامها. ثم افتح التطبيق. nano app.js ستبدو الأسطر الأولى من الملف كما يلي: الملف ‎~/node_project/app.js: const express = require('express'); const app = express(); const router = express.Router(); const path = __dirname + '/views/'; ... أسفل السّطر الذي يحدّد القيمة الثابتة للكائن router، الموجود قريبًا من أعلى الملف، أضف السطر التالي: الملف ‎~/node_project/app.js: ... const router = express.Router(); const db = require('./db'); const path = __dirname + '/views/'; ... يطلب هذا الأمر من التطبيق استخدام معلومات اتصال قاعدة البيانات المحددة في db.js. احفظ الملف وأغلقه عند الانتهاء من التحرير. بوجود معلومات قاعدة البيانات وبعد إضافتها إلى مشروعك، تكون مستعدًا لإنشاء المخططات والنماذج التي ستشكل البيانات في المجموعة sharks. الخطوة الثالثة: إنشاء مخططات ونماذج Mongoose ستكون خطوتنا التالية هي التفكير في بنية المجموعة sharks التي سينشئها المستخدمون في قاعدة بيانات sharkinfo بواسطة مدخلاتهم. فما هي البنية التي نريد أن تكون لهذه المستندات التي أُنشئت؟ تتضمن صفحة معلومات سمك القرش في تطبيقنا الحالي بعض التفاصيل حول أسماك القرش المختلفة وسلوكياتها: وفقًا لذلك، سيكون بإمكان المستخدمين أن يضيفوا أسماك قرش جديدة مرفقة بتفاصيل حول سلوكها العام. وسيحدّد هذا الهدف كيفية إنشاء مخططنا. من أجل تمييز مخططاتك ونماذجك عن الأجزاء الأخرى لتطبيقك، أنشئ مجلّدا models في مجلّد المشروع الحالي: mkdir models بعد ذلك، افتح ملفًا باسم sharks.js لإنشاء مخططك ونموذجك: nano models/sharks.js استورد وحدة Mongose في الجزء العلوي من الملف: الملف ‎~/node_project/models/sharks.js: const mongoose = require('mongoose'); بعد ذلك، حدّد كائنًا Schema لاستخدامه أساسًا للمخطط الخاصّ بأسماك القرش: الملف ‎~/node_project/models/sharks.js: const mongoose = require('mongoose'); const Schema = mongoose.Schema; يمكنك الآن تحديد الحقول التي تريد تضمينها في مخطّطك. ونظرًا لأننا نريد إنشاء مجموعة تحتوي على أسماك قرش فردية ومعلومات حول سلوكياتها، فلنُضمِّن فيها مفتاحين واحدٌ للاسم name والآخر للسلوك character. أضف مخطط Shark التالي أسفل تعريفات الثوابت: الملف ‎~/node_project/models/sharks.js: ... const Shark = new Schema ({ name: { type: String, required: true }, character: { type: String, required: true }, }); يتضمن هذا التعريف معلومات حول نوع الإدخال الذي نتوقعه من المستخدمين، وهو في هذه الحالة، سلسلة نصّية، وهل هذا الإدخال ضروري أم لا. أخيرًا ، أنشئ نموذج Shark باستخدام دالة Mongoose التالية model()‎. سيتيح لك هذا النموذج الاستعلام عن المستندات من خلال مجموعتك والتحقق من صحة المستندات الجديدة. أضف السطر التالي في أسفل الملف: ... module.exports = mongoose.model('Shark', Shark) يجعل هذا السطر الأخير النموذج Shark متاحًا كوحدة نمطية باستخدام خاصية module.exports. إذ تحدّد هذه الخاصية القيم التي ستصدرها الوحدة النمطية، مما يجعلها متاحة للاستخدام في أي مكان آخر في التطبيق. سيبدو الملف النهائي models/sharks.js على هذا النحو: const mongoose = require('mongoose'); const Schema = mongoose.Schema; const Shark = new Schema ({ name: { type: String, required: true }, character: { type: String, required: true }, }); module.exports = mongoose.model('Shark', Shark) احفظ الملف وأغلقه عند الانتهاء من التحرير. بإنشائك لمخطط Shark ونموذجه، سيكون بإمكانك البدء في العمل على المنطق الذي تريده أن يحكم كيفية تعامل تطبيقك مع مدخلات المستخدم. الخطوة الرابعة: إنشاء وحدات التحكم ستكون خطوتنا التالية هي إنشاء وحدة التحكم التي تحدد كيفية حفظ مدخلات المستخدم في قاعدة البيانات وإعادتها إليه. أنشئ أولاً مجلّدًا لوحدة التحكم: mkdir controllers بعد ذلك، افتح ملفًا في هذا المجلد باسم sharks.js: nano controllers/sharks.js في الجزء العلوي من الملف، سنستورد الوحدة مع النموذج Shark لكي نستطيع استخدامها في منطق وحدة التحكم لدينا. سنستورد أيضًا وحدة المسار path للوصول إلى الأدوات المساعدة التي ستتيح لنا تعيين المسار إلى الاستمارة التي سيدخل فيها المستخدمون معلوماتهم عن أسماك القرش. أضف الدوالّ require التالية إلى بداية الملف: الملف ‎~/node_project/controllers/sharks.js: const path = require('path'); const Shark = require('../models/sharks'); بعد ذلك، سنكتب سلسلة من الدوال التي سنصدّرها مع وحدة التحكم باستخدام اختصارات Node للتصدير exports. ستشمل هذه الدوالّ المهام الثلاثة المتعلقة ببيانات سمك القرش الخاصة بمستخدمنا: إرسال إستمارة إدخال سمك القرش للمستخدمين. إنشاء إدخالٍ لقرش جديد. إعادة عرض أسماك القرش للمستخدمين. من أجل البدء، أنشئ دالةً index لعرض صفحة أسماك القرش مع إستمارة الإدخال. أضف هذه الدّالة أسفل الاستيرادات: ... exports.index = function (req, res) { res.sendFile(path.resolve('views/sharks.html')); }; بعد ذلك أضف، أسفل الدالة index، دالّة بالاسم create لإنشاء إدخال جديد لسمك القرش في المجموعة Sharks: ... exports.create = function (req, res) { var newShark = new Shark(req.body); console.log(req.body); newShark.save(function (err) { if(err) { res.status(400).send('Unable to save shark to database'); } else { res.redirect('/sharks/getshark'); } }); }; سوف تستدعى هذه الدّالة عندما يرسل المستخدم بيانات سمك القرش إلى الإستمارة على الصفحة sharks.html. سننشئ لاحقًا المسار route الخاصّ بهذا الإرسال POST في هذا البرنامج التعليمي عندما ننشئ مسارات التطبيق. باستخدام المحتوى body الخاصّ بالطلب POST، ستنشئ الدالة create كائن ملفّ سمك القرش الجديد، الذي يأخذ هنا الاسم newShark، اعتمادًا على النموذج Shark الذي استوردناه. ولقد أضفنا التابع console.log من أجل إظهار الإدخال الخاصّ بسمك القرش إلى الطرفية من أجل التحقق من أن طريقة POST تعمل على النحو المنشود، ولكن تبقى لك الحرّية في تجاوز هذا الأمر إذا كنت تفضل ذلك. باستخدام الكائنnewShark ، ستستدعي الدالة create بعد ذلك تابع Mongoose المسمّى model.save()‎ من أجل إنشاء ملفّ سمك قرش جديد اعتمادًا على المفاتيح التي حدّدتها في النموذج Shark. تتبع دالّة ردّ النداء هذه نموذج ردّ النداء المعياري في Node وهو: callback(error, results)‎. في حالة وجود خطأ، نرسل رسالة تُبلغ المستخدمين عن الخطأ، وفي حالة نجاح العمليّة، نستخدم التابع res.redirect()‎ لتوجيه المستخدمين إلى نقطة النهاية التي ستعيد معلومات القرش إليهم على المتصفح. في الختام، ستعرض الدّالة list محتويات المجموعة مرة أخرى للمستخدم. أضف الشيفرة التالية أسفل الدالة create: ... exports.list = function (req, res) { Shark.find({}).exec(function (err, sharks) { if (err) { return res.send(500, err); } res.render('getshark', { sharks: sharks }); }); }; تستخدم هذه الدالّة النموذج Shark بالاعتماد على التابع ()model.find الخاصّ بMongoose من أجل إرجاع معلومات أسماك القرش التي أُدخِلت في المجموعة sharks. يتمّ هذا الأمر عبر إرجاع كائن الاستعلام query object، ويمثل في هذه الحالة جميع الإدخالات في المجموعة sharks، باستخدام الدالّة ()exec الخاصّة ب Mongoose. في حالة وجود خطأ، سترسل دالّة رد النّداء خطأً 500. سوف يُعرض كائن الاستعلام الذي أرجعته الدّالة مع المجموعة sharks في الصفحة getshark التي سننشئها في الخطوة التالية باستخدام لغة القوالب EJS. سيبدو الملف النهائي ‎~/node_project/controllers/sharks.js على هذا النحو: const path = require('path'); const Shark = require('../models/sharks'); exports.index = function (req, res) { res.sendFile(path.resolve('views/sharks.html')); }; exports.create = function (req, res) { var newShark = new Shark(req.body); console.log(req.body); newShark.save(function (err) { if(err) { res.status(400).send('Unable to save shark to database'); } else { res.redirect('/sharks/getshark'); } }); }; exports.list = function (req, res) { Shark.find({}).exec(function (err, sharks) { if (err) { return res.send(500, err); } res.render('getshark', { sharks: sharks }); }); }; ضع في اعتبارك أنه رغم أننا لا نستخدم الدّوال السهمية هنا، فقد ترغب في تضمينها أثناء تكرارك لهذه الشيفرة في عمليتك التطويرية. احفظ الملف وأغلقه عند الانتهاء من التحرير. قبل الانتقال إلى الخطوة التالية، يمكنك تنفيذ الأمر tree مرة أخرى من المجلّد nodeproject لعرض بنية المشروع في هذه المرحلة. هذه المرة، ومن أجل الإيجاز، سنطلب من tree استبعاد المجلّد nodemodules عبر الراية ‎-I: tree -I node_modules مع الإضافات التي أجريتها، ستبدو بنية مشروعك على النحو التالي: ├── Dockerfile ├── README.md ├── app.js ├── controllers │ └── sharks.js ├── db.js ├── models │ └── sharks.js ├── package-lock.json ├── package.json └── views ├── css │ └── styles.css ├── index.html └── sharks.html الآن بعد أن أصبحت لديك وحدة تحكم لتوجيه كيفية حفظ مدخلات المستخدم وإعادتها إليه، يمكنك الانتقال إلى إنشاء طرق العرض التي ستبلور منطق وحدة التحكم الخاصة بك. الخطوة الخامسة: استخدام برمجيات EJS و Express الوسيطة لجمع البيانات وعرضها من أجل تمكين التطبيق من العمل ببيانات المستخدم، سنقوم بأمرين مهمّين: سنضمّن أولاً دالة وسيطة Express تسمى ()urlencoded، والتي ستمكن تطبيقنا من تحليل بيانات المستخدم المُدخلَة. ثم سنضيف ثانيا علامات القوالب (template tags) إلى واجهة العرض views لتمكين التفاعل الديناميكي مع بيانات المستخدم في الشيفرة. من أجل استخدام الدالة ()urlencoded الخاصة بـ Express، افتح أولاً الملف app.js: nano app.js أضف السطر التالي فوق الدالة ()express.static في الملف ‎~/node_project/app.js: ... app.use(express.urlencoded({ extended: true })); app.use(express.static(path)); ... ستمكّن إضافة هذه الدّالة الوصول إلى بيانات POST المحلّلة (parsed) في استمارة معلومات سمك القرش. نحدّد القيمة true في الخيار extended لإتاحة قدر أكبر من المرونة في نوع البيانات التي سيحللها التطبيق (بما في ذلك الكائنات مثل الكائنات المتداخلة). يرجى الاطلاع على توثيق الدّالة لمزيد من المعلومات حول الخيارات. احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد ذلك، سنضيف إمكانية استخدام القالب إلى واجهات العرض. ثبّت أولاً حزمة ejs باستخدام instaill npm: npm install ejs بعد ذلك ، افتح ملف sharks.html في المجلد views: nano views/sharks.html لقد ألقينا نظرة من قبل في الخطوة الثالثة على هذه الصفحة لتحديد كيفية كتابة المخطط والنموذج Mongoose: الآن ، بدلاً من وجود تنسيق بعمودين، سنضيف عمودًا ثالثًا يحتوي على استمارة يمكن للمستخدمين إدخال معلومات حول أسماك القرش فيها. كخطوة أولى، عدّل أبعاد الأعمدة الموجودة إلى 4 لإنشاء ثلاثة أعمدة متساوية الحجم. لاحظ أنك ستحتاج إلى إجراء هذا التعديل على السطرين اللذين يقرآن حاليًا <div class="col-lg-6"‎>. سيصبح كلاهما <div class="col-lg-4"‎>: الملف ‎~/node_project/views/sharks.html: ... <div class="container"> <div class="row"> <div class="col-lg-4"> <p> <div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans. </div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sawshark.jpg" alt="Sawshark"> </p> </div> <div class="col-lg-4"> <p> <div class="caption">Other sharks are known to be friendly and welcoming!</div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark"> </p> </div> </div> </div> </html> للاطلاع على توضيح أكثر لنظام الشبكة في Bootstrap، بما في ذلك تخطيطات الصفوف والأعمدة، يرجى الاطلاع على هذه المقدمة إلى إطار Bootstrap. بعد ذلك، أضف عمودًا آخر يتضمن نقطة النهاية المحددة للطلب POST مع بيانات المستخدم عن سمك القرش ووسوم القالب EJS التي ستلتقط تلك البيانات. سيكون هذا العمود أسفل وسمي الإغلاق <‎/p> و <‎/div> للعمود السابق وفوق وسوم الإغلاق للصف والحاوية والصفحة HTML. وتوجد وسوم الإغلاق هذه بالفعل في الشّيفرة. اتركها في مكانها وأنت تضيف الشيفرة التالية لإنشاء العمود الجديد: ... </p> <!-- closing p from previous column --> </div> <!-- closing div from previous column --> <div class="col-lg-4"> <p> <form action="/sharks/addshark" method="post"> <div class="caption">Enter Your Shark</div> <input type="text" placeholder="Shark Name" name="name" <%=sharks[i].name; %> <input type="text" placeholder="Shark Character" name="character" <%=sharks[i].character; %> <button type="submit">Submit</button> </form> </p> </div> </div> <!-- closing div for row --> </div> <!-- closing div for container --> </html> <!-- closing html tag --> تضيف هنا في الوسم form نقطة نهاية "‎/sharks/addhark" لبيانات سمك القرش الخاصة بالمستخدم وتحدّد الطريقة POST لإرسالها. كما تحدّد في حقول الإدخال، الحقلين "Shark Name" و"Shark Character" ، وفقًا للنموذج Shark الذي حدّدته سابقًا. لإضافة مُدخلات المستخدم إلى المجموعة sharks، فأنت تستخدم وسوم قالب:EJS (<%=, %>) بناءً على قواعد لغة جافاسكربت لتوجيه مدخلات المستخدم إلى الحقول المناسبة في المستند الذي أنشئ حديثًا. لمزيد من المعلومات حول كائنات جافاسكربت، يرجى مراجعة توثيق جافاسكربت في الموسوعة. ولمعرفة المزيد عن وسوم قالبEJS ، يرجى الاطلاع على التوثيق الرسمي لEJS. ستبدو الحاوية بأكملها مع جميع الأعمدة الثلاثة، بما في ذلك العمود الذي يحتوي على استمارة إدخال سمك القرش، على هذا النحو عند الانتهاء: الملف ‎~/node_project/views/sharks.html: ... <div class="container"> <div class="row"> <div class="col-lg-4"> <p> <div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans. </div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sawshark.jpg" alt="Sawshark"> </p> </div> <div class="col-lg-4"> <p> <div class="caption">Other sharks are known to be friendly and welcoming!</div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark"> </p> </div> <div class="col-lg-4"> <p> <form action="/sharks/addshark" method="post"> <div class="caption">Enter Your Shark</div> <input type="text" placeholder="Shark Name" name="name" <%=sharks[i].name; %> <input type="text" placeholder="Shark Character" name="character" <%=sharks[i].character; %> <button type="submit">Submit</button> </form> </p> </div> </div> </div> </html> احفظ الملف وأغلقه عند الانتهاء من التحرير. الآن بعد أن أصبح لديك طريقة لجمع مدخلات المستخدم، يمكنك إنشاء نقطة نهاية (endpoint) لعرض أسماك القرش المُعادة وكذلك معلومات السلوك المرتبطة بها. انسخ ملف sharks.html المعدّل حديثًا إلى ملف يسمى getshark.html: cp views/sharks.html views/getshark.html افتح الملف getshark.html: nano views/getshark.html داخل الملف، سنعدّل العمود الذي استخدمناه لإنشاء استمارة إدخال أسماك القرش من خلال استبداله بعمود سيعرض أسماك القرش في المجموعة sharks. مرة أخرى، ستمتدّ الشفرة بين وسمي <p/> و <‎/div> للعمود السابق ووسوم الإغلاق للصف والحاوية والصفحة HTML. احرص على ترك هذه الوسوم في مكانها وأنت تضيف الشيفرة التالية لإنشاء العمود: ... </p> <!-- closing p from previous column --> </div> <!-- closing div from previous column --> <div class="col-lg-4"> <p> <div class="caption">Your Sharks</div> <ul> <% sharks.forEach(function(shark) { %> <p>Name: <%= shark.name %></p> <p>Character: <%= shark.character %></p> <% }); %> </ul> </p> </div> </div> <!-- closing div for row --> </div> <!-- closing div for container --> </html> <!-- closing html tag --> هنا تستخدم وسوم قالب EJS والدالّة forEach()‎ لإخراج كل قيمة في المجموعة sharks ، بما في ذلك معلومات حول أحدث أسماك القرش المضافة. ستبدو الحاوية بأكملها التي تحتوي على جميع الأعمدة الثلاثة، بما في ذلك العمود الذي يتضمن المجموعة sharks، على هذا النحو عند الانتهاء: ... <div class="container"> <div class="row"> <div class="col-lg-4"> <p> <div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans. </div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sawshark.jpg" alt="Sawshark"> </p> </div> <div class="col-lg-4"> <p> <div class="caption">Other sharks are known to be friendly and welcoming!</div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark"> </p> </div> <div class="col-lg-4"> <p> <div class="caption">Your Sharks</div> <ul> <% sharks.forEach(function(shark) { %> <p>Name: <%= shark.name %></p> <p>Character: <%= shark.character %></p> <% }); %> </ul> </p> </div> </div> </div> </html> احفظ الملف وأغلقه عند الانتهاء من التحرير. لكي يستخدم التطبيق القوالب التي أنشأتها، ستحتاج إلى إضافة بضعة أسطر إلى ملفّك app.js. لذا افتحه مرة أخرى: nano app.js فوق المكان الذي أضفت فيه الدالة express.urlencoded()‎، أضف الآن الأسطر التالية: ... app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(express.urlencoded({ extended: true })); app.use(express.static(path)); ... تطلب الدّالة app.engine من التطبيق توجيه محرك قوالب EJS إلى ملفات HTML، بينما تحدّد app.set محرك العرض الافتراضي. يجب أن يبدو ملف app.js الآن على النحو التالي: الملف ‎~/node_project/app.js: const express = require('express'); const app = express(); const router = express.Router(); const db = require('./db'); const path = __dirname + '/views/'; const port = 8080; router.use(function (req,res,next) { console.log('/' + req.method); next(); }); router.get('/',function(req,res){ res.sendFile(path + 'index.html'); }); router.get('/sharks',function(req,res){ res.sendFile(path + 'sharks.html'); }); app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(express.urlencoded({ extended: true })); app.use(express.static(path)); app.use('/', router); app.listen(port, function () { console.log('Example app listening on port 8080!') }) الآن وبعد أن أنشأت واجهات عرض يمكن أن تعمل ديناميكيًا مع بيانات المستخدم، فقد حان الوقت لإنشاء المسارات لمشروعك من أجل الجمع بين واجهات العرض ومنطق وحدة التحكم الذي تعمل به. الخطوة السادسة: إنشاء المسارات (routes) الخطوة الأخيرة في الجمع بين مكونات التطبيق هي إنشاء المسارات. سنحاول فصل المسارات حسب وظيفتها، بما في ذلك المسار إلى صفحة الهبوط لتطبيقنا ومسار آخر إلى صفحة أسماك القرش. سيكون المسار sharks هو المكان الذي ندمج فيه منطق وحدة التحكم مع واجهات العرض التي أنشأناها في الخطوة السابقة. أنشئ في البداية مجلّدًا باسم routes: mkdir routes بعد ذلك، أنشئ ملفًا يسمى index.js في هذا المجلّد وافتحه: nano routes/index.js سيستورد هذا الملف في البداية كائنات express وrouter وpath، مما يتيح لنا تحديد المسارات التي نريد تصديرها باستخدام الكائن router، وهذا يتيح بدوره العمل ديناميكيًا بمسارات الملفات (file paths). أضف الشيفرة التالية في أعلى الملف: الملف ‎~/node_project/routes/index.js: const express = require('express'); const router = express.Router(); const path = require('path'); بعد ذلك، أضف الدّالة router.use التالية، والتي تُحمّل الدالّة الوسيطة التي ستسجل طلبات الموجه وتمررها إلى مسار التطبيق: ... router.use (function (req,res,next) { console.log('/' + req.method); next(); }); ستُوجَّه الطلبات الخاصّة بجذر التطبيق إلى هنا في البداية، ومن ثم سيُوجّه المستخدمون إلى صفحة الهبوط لتطبيقنا، وهو المسار الذي سنحدّده بعد ذلك. أضف الشيفرة التالية أسفل الدّالة router.use لتحديد المسار نحو صفحة الهبوط: ... router.get('/',function(req,res){ res.sendFile(path.resolve('views/index.html')); }); إن أول ما نريد إرساله إليه المستخدم عندما يزور التطبيق هو صفحة الهبوط index.html الموجودة في المجلّد views. ختامًا، لجعل هذه المسارات قابلة للوصول كوحدات يمكن استيرادها في مكان آخر من التطبيق، أضف عبارة ختامية إلى نهاية الملف لتصدير الكائن router: ... module.exports = router; سوف يبدو الملف النهائي على هذا النحو: const express = require('express'); const router = express.Router(); const path = require('path'); router.use (function (req,res,next) { console.log('/' + req.method); next(); }); router.get('/',function(req,res){ res.sendFile(path.resolve('views/index.html')); }); module.exports = router; احفظ هذا الملف وأغلقه عندما تنتهي من التحرير. بعد ذلك، افتح ملفًا باسم sharks.js لتحديد كيفية استخدام التطبيق لنقاط النهاية وواجهات العرض المختلفة التي أنشأناها للتعامل مع مدخلات المستخدم لأسماك القرش: nano routes/sharks.js في الجزء العلوي من الملف، استورد الكائنات epress وrouter: const express = require('express'); const router = express.Router(); بعد ذلك، استورد وحدة نمطية تسمى shark لتتيح لك العمل بالدّوال المصدرة التي حددتها في وحدة التحكم: const express = require('express'); const router = express.Router(); const shark = require('../controllers/sharks'); يمكنك الآن إنشاء مسارات باستخدام الدّوال index وcreate وlist التي حدّدتها في ملف وحدة التحكم sharks. سيُربط كل مسار بطريقة HTTP المناسبة والتي تكون إما GET في حالة عرض صفحة الهبوط لمعلومات أسماك القرش الرئيسية وإعادة قائمة أسماك القرش إلى المستخدم، وإما POST في حالة إنشاء إدخال جديد لسمك قرش: ... router.get('/', function(req, res){ shark.index(req,res); }); router.post('/addshark', function(req, res) { shark.create(req,res); }); router.get('/getshark', function(req, res) { shark.list(req,res); }); يستخدم كل مسار الدّالة المرتبطة به في controllers/sharks.js، لأننا جعلنا هذه الوحدة قابلة للوصول عن طريق استيرادها في الجزء العلوي من هذا الملف. ختامًا، أغلق الملف بربط هذه المسارات بالكائن routes وتصديرها: ... module.exports = router; سيبدو الملف النهائي على هذا النحو: const express = require('express'); const router = express.Router(); const shark = require('../controllers/sharks'); router.get('/', function(req, res){ shark.index(req,res); }); router.post('/addshark', function(req, res) { shark.create(req,res); }); router.get('/getshark', function(req, res) { shark.list(req,res); }); module.exports = router; احفظ الملف وأغلقه عند الانتهاء من التحرير. ستكون الخطوة الأخيرة في جعل هذه المسارات متاحةً للتطبيق هي إضافتها إلى app.js. افتح هذا الملف مرة أخرى: nano app.js أسفل الثابتة db، أضف الاستيراد التالي لمساراتك: ... const db = require('./db'); const sharks = require('./routes/sharks'); بعد ذلك، عوّض الدّالة app.use التي تُركّب الكائن router بالسطر التالي الذي سيركّب وحدة التوجيه sharks: ... app.use(express.static(path)); app.use('/sharks', sharks); app.listen(port, function () { console.log("Example app listening on port 8080!") }) تستطيع الآن حذف المسارات التي حدّدتها مسبقًا في هذا الملف، نظرًا لأنك تستورد مسارات تطبيقك باستخدام وحدة التوجيه sharks. ستبدو النسخة النهائية للملف app.js على النحو التالي: const express = require('express'); const app = express(); const router = express.Router(); const db = require('./db'); const sharks = require('./routes/sharks'); const path = __dirname + '/views/'; const port = 8080; app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(express.urlencoded({ extended: true })); app.use(express.static(path)); app.use('/sharks', sharks); app.listen(port, function () { console.log('Example app listening on port 8080!') }) احفظ الملف وأغلقه عند الانتهاء من التحرير. يمكنك الآن إعادة تنفيذ الأمر tree مرة أخرى لرؤية البنية النهائية لمشروعك: tree -I node_modules ستبدو بنية مشروعك الآن على هذا النحو: ├── Dockerfile ├── README.md ├── app.js ├── controllers │ └── sharks.js ├── db.js ├── models │ └── sharks.js ├── package-lock.json ├── package.json ├── routes │ ├── index.js │ └── sharks.js └── views ├── css │ └── styles.css ├── getshark.html ├── index.html └── sharks.html بإنشائك لجميع مكونات تطبيقك ووضعها في المكان المناسب، فأنت الآن مستعدّ لإضافة سمك قرش للاختبار إلى قاعدة بياناتك! إذا تتبعت درس إعداد الخادم الأولي في المتطلبات الأساسية، فستحتاج إلى تعديل جدار الحماية الخاص بك، لأنه لا يسمح حاليًا سوى بحركة المرور SSH. لذا نفّذ هذا الأمر من أجل السماح بحركة المرور إلى منفذ 8080: sudo ufw allow 8080 شغّل التطبيق: node app.js بعد ذلك، انتقل بمتصفّحك إلى http://yourserverip:8080. ستظهر لك صفحة الهبوط التالية: انقر على زر الحصول على معلومات القرش. ستظهر لك صفحة المعلومات التالية، مع استمارة إدخال سمك القرش المضافة: أضف في الاستمارة، سمكة قرش من اختيارك. لأغراض هذا العرض التوضيحي، سنضيف Megalodon Shark إلى حقل اسم القرش، وAncient إلى حقل سلوك القرش: انقر على زر الإرسال. ستظهر لك صفحة بها معلومات القرش معروضة لك: سترى أيضًا مخرجاتٍ في وحدة التحكم الخاصة بك تشير إلى أنّ سمك القرش أضيف إلى مجموعتك: Example app listening on port 8080! { name: 'Megalodon Shark', character: 'Ancient' } إذا كنت ترغب في إنشاء إدخال جديد لسمك القرش، فارجع إلى صفحة أسماك القرش وكرر عملية إضافة سمكة قرش. لديك الآن تطبيق لمعلومات سمك القرش يتيح للمستخدمين بإضافة معلومات حول أسماك القرش المفضلة لديهم. خاتمة لقد أنشأت في هذا البرنامج التعليمي تطبيق Node من خلال دمج قاعدة بيانات MongoDB وإعادة كتابة منطق التطبيق باستخدام النظام الهيكلي MVC. ويمكن أن يكون هذا التطبيق بمثابة نقطة انطلاق جيدة لتطبيق CRUD مكتمل. لمزيد من التفصيل حول النظام الهيكلي MVC في سياقات أخرى، يرجى الاطلاع على سلسلة مقالات تطوير Django. لمزيد من المعلومات حول العمل على MongoDB، يرجى الاطلاع على سلسلة المقالات حول MongoDB. ترجمة -وبتصرف- للمقال How To Integrate MongoDB with Your Node Application لصاحبته Kathleen Juell
  3. تتيح منصة Docker للمطورين تحزيم وتنفيذ التطبيقات على شكل حاويات (containers). وتعدّ الحاوية عملية (process) منعزلة تعمل على نظام تشغيل مشترك، وتوفر بديلاً أخفّ من الأجهزة الافتراضية (virtual machines) . رغم أن الحاويات ليست جديدة، إلا أن لها فوائد عديدة، من بينها عزل العمليات وتوحيد البيئة، وتزداد أهميتها نظرًا لتزايد إقبال المطورين على استخدام بنية التطبيقات الموزعة. عند بناء تطبيق وتوسيع نطاقه باستخدام Docker، تكون نقطة البداية عادةً هي إنشاء صورة للتطبيق، والتي يمكنك تنفيذها بعد ذلك في حاوية. تتضمن الصورة شيفرة التطبيق والمكتبات وملفات التكوين ومتغيرات البيئة ووقت التشغيل. ويضمن استخدام صورة ما أن تكون البيئة في حاويتك موحدة ومتضمّنة فقط لما هو ضروري لإنشاء التطبيق وتنفيذه. ستنشئ في هذا الدرس صورة تطبيق لموقع ويب ثابت يستخدم إطار عمل Express و Bootstrap. وستنشئ بعد ذلك حاوية باستخدام تلك الصورة ورفعها إلى Docker Hub للاستخدام المستقبلي. أخيرًا، ستتمكّن من سحب الصورة المخزنة من مستودع Docker Hub الخاص بك وتنشئ حاوية أخرى، مع توضيح كيف يمكنك إعادة إنشاء التطبيق وتوسيع نطاقه. المتطلبات الأساسية سوف تحتاج من أجل متابعة هذه السلسلة إلى العناصر التالية: خادم Ubuntu 18.04 يمكنك إعداده باتباع دليل إعداد الخادم الأولي. تثبيت Docker على الخادم الخاص بك، باتباع الخطوتين 1 و 2 في دليل كيفية تثبيت واستخدام Docker على Ubuntu 18.04. تثبيت Node.js و npm، باتباع هذه الإرشادات تحديدًا التّثبيت باستخدام أرشيف الحزم الشّخصي PPA المدار بواسطة NodeSource. حساب Docker Hub. لإلقاء نظرة عامة حول كيفية إعداده، راجع هذه المقدمة عن بدء استخدام Docker Hub. الخطوة الأولى: تثبيت الاعتماديات لتطبيقك لإنشاء الصورة، ستحتاج أولاً إلى إنشاء ملفات التطبيق الخاصة بك والتي يمكنك بعد ذلك نسخها إلى حاويتك. ستتضمن هذه الملفات المحتوى الثابت للتطبيق والشيفرة والاعتماديات. أنشئ في البداية مجلًدًا لمشروعك في المجلّد الرئيسي للمستخدم غير الجذري. سنستدعي node_project الخاص بنا، لكن يمكنك استبداله بشيء آخر: $ mkdir node_project انتقل إلى هذا المجلّد: $ cd node_project سيكون هذا هو المجلّد الرئيسي للمشروع. أنشئ بعد ذلك ملف [package.json](https://docs.npmjs.com/files/package.json) باعتماديات مشروعك ومعلومات التعريف الأخرى. افتح الملف باستخدام nano أو المحرر المفضل لديك: $ nano package.json أضف المعلومات التالية حول المشروع، بما في ذلك اسم المشروع وصاحبه والترخيص ونقطة الدخول والاعتماديات. تأكد من استبدال معلومات صاحب المشروع باسمك ومعلومات الاتصال الخاصة بك: الملف ‎:~/node_project/package.json { "name": "nodejs-image-demo", "version": "1.0.0", "description": "nodejs image demo", "author": "Sammy the Shark <sammy@example.com>", "license": "MIT", "main": "app.js", "keywords": [ "nodejs", "bootstrap", "express" ], "dependencies": { "express": "^4.16.4" } } يحتوي هذا الملف على اسم المشروع وصاحبه والترخيص الذي تتم مشاركته بموجبه. ينصح npm بجعل اسم المشروع قصيرًا وذا دلالة وصفية مع تجنب التكرار في سجل npm. لقد أدرجنا ترخيص MIT في حقل الترخيص، مما يتيح الاستخدام المجاني لشيفرة التطبيق وتوزيعه. بالإضافة إلى ذلك، يحدد الملف العناصر التالية: "main": نقطة دخول التطبيق app.js، ستقوم بإنشاء هذا الملف بعد ذلك. "dependencies": اعتماديات المشروع، في هذه الحالة، Express إصدار 4.16.4 أو أعلى منه. ورغم أن هذا الملف لا يحتوي على مستودعٍ، فيمكنك إضافته باتباع هذه الإرشادات حول إضافة مستودع إلى ملف package.json الخاص بك. وتعدّ هذه إضافةً جيدةً إذا كنت تقوم بإصدار تطبيقك. احفظ الملف وأغلقه عندما تنتهي من إجراء التعديلات. لتثبيت اعتماديات مشروعك، نفّذ الأمر التالي: $ npm install سيؤدي ذلك إلى تثبيت الحزم التي أدرجتها في ملف package.json في مجلّد المشروع الخاص بك. يمكننا الآن الانتقال إلى بناء ملفات التطبيق. الخطوة الثانية: إنشاء ملفات التطبيق سنعمل على إنشاء موقع ويب يقدم للمستخدمين معلومات حول أسماك القرش. سيكون لدى لتطبيق نقطة دخول رئيسية و ملفّ app.js ومجلّد views يتضمن الأصول الثابتة للمشروع. ستوفر صفحة الهبوط index.html للمستخدمين بعض المعلومات الأولية ورابطًا نحو صفحة تحتوي على معلومات أكثر تفصيلًا عن أسماك القرش، sharks.html. سننشئ في مجلّد views كلًّا من صفحة الهبوط و الصفحة sharks.html. في البداية، افتح ملفّ app.js في مجلّد المشروع الرئيسي لتحديد مسارات المشروع: $ nano app.js سيعمل الجزء الأول من الملف على إنشاء تطبيق Express وكائنات الموجِّه، وتحديد المجلّد الأساسي والمنفذ كثوابت: الملف ‎:~/node_project/app.js const express = require('express'); const app = express(); const router = express.Router(); const path = __dirname + '/views/'; const port = 8080; تحمّل الداّلة require الوحدة express التي نستخدمها فيما بعد لإنشاء الكائنات app و router. سيؤدي الكائن router وظيفة التوجيه الخاصة بالتطبيق، وعندما نحدد مسارات طرق HTTP، سنضيفها إلى هذا الكائن لتحديد كيفية تعامل التطبيق مع الطلبات. يعين هذا الجزء من الملف أيضًا ثابتتي المسار path والمنفذ port: *path: يحدد المجلّد الأساسي، والذي سيكون المجلّد الفرعي ل views داخل مجلّد المشروع الحالي. port: يطلب من التطبيق الاستماع على المنفذ 8080. بعد ذلك، حدّد مسارات التطبيق باستخدام الكائن router: الملف ‎~/node_project/app.js: router.use(function (req,res,next) { console.log('/' + req.method); next(); }); router.get('/', function(req,res){ res.sendFile(path + 'index.html'); }); router.get('/sharks', function(req,res){ res.sendFile(path + 'sharks.html'); }); تحمّل الدالّة router.use دالّة وسيطة تعمل على تسجيل طلبات الموجه وتمررها إلى مسارات التطبيق. تُحدّد هذه المسارات في الدوالّ الموالية، والتي تنصّ على أن تنفيذ الأمر GET على عنوان URL للمشروع الأساسي يجب أن يُرجع الصفحة index.html، بينما يجب أن يُرجع تنفيذ الأمر GET على المسار sharks/ الصفحة sharks.html. ختامًا، صل بين البرمجية الوسيطة router والأصول الثابتة للتطبيق واجعل التطبيق يستمع على المنفذ 8080: ~/node_project/app.js ... app.use(express.static(path)); app.use('/', router); app.listen(port, function () { console.log('Example app listening on port 8080!') }) سيبدو ملف app.js النهائي كما يلي: const express = require('express'); const app = express(); const router = express.Router(); const path = __dirname + '/views/'; const port = 8080; router.use(function (req,res,next) { console.log('/' + req.method); next(); }); router.get('/', function(req,res){ res.sendFile(path + 'index.html'); }); router.get('/sharks', function(req,res){ res.sendFile(path + 'sharks.html'); }); app.use(express.static(path)); app.use('/', router); app.listen(port, function () { console.log('Example app listening on port 8080!') }) احفظ الملف وأغلقه عند الانتهاء. بعد ذلك، دعنا نضيف بعض المحتوى الثابت إلى التطبيق. ابدأ بإنشاء المجلد views: $ mkdir views افتح ملف صفحة الهبوط index.html: $ nano views/index.html أضف الشيفرة التالية إلى الملف، الذي سيعمل على استيراد Bootstrap وإنشاء مكوّن jumbotron مع رابط إلى صفحة المعلومات المفصّلة sharks.html: الملف ‎~/node_project/views/index.html: <!DOCTYPE html> <html lang="en"> <head> <title>About Sharks</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <link href="css/styles.css" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Merriweather:400,700" rel="stylesheet" type="text/css"> </head> <body> <nav class="navbar navbar-dark bg-dark navbar-static-top navbar-expand-md"> <div class="container"> <button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> </button> <a class="navbar-brand" href="#">Everything Sharks</a> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav mr-auto"> <li class="active nav-item"><a href="/" class="nav-link">Home</a> </li> <li class="nav-item"><a href="/sharks" class="nav-link">Sharks</a> </li> </ul> </div> </div> </nav> <div class="jumbotron"> <div class="container"> <h1>Want to Learn About Sharks?</h1> <p>Are you ready to learn about sharks?</p> <br> <p><a class="btn btn-primary btn-lg" href="/sharks" role="button">Get Shark Info</a> </p> </div> </div> <div class="container"> <div class="row"> <div class="col-lg-6"> <h3>Not all sharks are alike</h3> <p>Though some are dangerous, sharks generally do not attack humans. Out of the 500 species known to researchers, only 30 have been known to attack humans. </p> </div> <div class="col-lg-6"> <h3>Sharks are ancient</h3> <p>There is evidence to suggest that sharks lived up to 400 million years ago. </p> </div> </div> </div> </body> </html> يتيح شريط التنقل في الأعلى للمستخدمين المرور بين الصفحة الرئيسية وصفحة أسماك القرش. في المكون الفرعي navbar-nav، نستخدم الصنف active في Bootstrap للإشارة إلى الصفحة الحالية للمستخدم. لقد حددنا أيضًا الطرق المؤدية إلى الصفحات الثابتة لدينا، والتي تتوافق مع الطرق التي حددناها في app.js: الملف ‎~/node_project/views/index.html: ... <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav mr-auto"> <li class="active nav-item"><a href="/" class="nav-link">Home</a> </li> <li class="nav-item"><a href="/sharks" class="nav-link">Sharks</a> </li> </ul> </div> ... بالإضافة إلى ذلك، أنشأنا رابطًا إلى صفحة معلومات سمك القرش في زر jumbotron الخاص بنا: الملف ‎~/node_project/views/index.html: ... <div class="jumbotron"> <div class="container"> <h1>Want to Learn About Sharks?</h1> <p>Are you ready to learn about sharks?</p> <br> <p><a class="btn btn-primary btn-lg" href="/sharks" role="button">Get Shark Info</a> </p> </div> </div> ... يوجد أيضًا رابط إلى ورقة أنماط مخصصة في عنصر الترويسة header: الملف ‎~/node_project/views/index.html: ... <link href="css/styles.css" rel="stylesheet"> ... سننشئ ورقة الأنماط هذه في نهاية هذه الخطوة. احفظ الملف وأغلقه عند الانتهاء. يمكننا، من خلال صفحة الهبوط، إنشاء صفحة معلومات أسماك القرش sharks.html، والتي ستوفر للمستخدمين المهتمين مزيدًا من المعلومات حول أسماك القرش. افتح الملف: $ nano views/sharks.html الملف ‎~/node_project/views/sharks.html: <!DOCTYPE html> <html lang="en"> <head> <title>About Sharks</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <link href="css/styles.css" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Merriweather:400,700" rel="stylesheet" type="text/css"> </head> <nav class="navbar navbar-dark bg-dark navbar-static-top navbar-expand-md"> <div class="container"> <button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> </button> <a class="navbar-brand" href="/">Everything Sharks</a> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav mr-auto"> <li class="nav-item"><a href="/" class="nav-link">Home</a> </li> <li class="active nav-item"><a href="/sharks" class="nav-link">Sharks</a> </li> </ul> </div> </div> </nav> <div class="jumbotron text-center"> <h1>Shark Info</h1> </div> <div class="container"> <div class="row"> <div class="col-lg-6"> <p> <div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans. </div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sawshark.jpg" alt="Sawshark"> </p> </div> <div class="col-lg-6"> <p> <div class="caption">Other sharks are known to be friendly and welcoming!</div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark"> </p> </div> </div> </div> </html> لاحظ أنه في هذا الملف، نستخدم مرة أخرى الصنف active للإشارة إلى الصفحة الحالية. احفظ الملف وأغلقه عند الانتهاء. ختامًا ، أنشئ ورقة الأنماط المخصصة CSS التي ربطتها بالصفحتين index.html و sharks.html، وذلك بإنشاء ملف css أولاً في المجلّد views: $ mkdir views/css افتح ورقة الأنماط: nano views/css/styles.css أضف الشيفرة التالية التي ستحدد اللون والخط المطلوب لصفحاتنا: الملف ‎~/node_project/views/css/styles.css: .navbar { margin-bottom: 0; } body { background: #020A1B; color: #ffffff; font-family: 'Merriweather', sans-serif; } h1, h2 { font-weight: bold; } p { font-size: 16px; color: #ffffff; } .jumbotron { background: #0048CD; color: white; text-align: center; } .jumbotron p { color: white; font-size: 26px; } .btn-primary { color: #fff; text-color: #000000; border-color: white; margin-bottom: 5px; } img, video, audio { margin-top: 20px; max-width: 80%; } div.caption: { float: left; clear: both; } بالإضافة إلى تعيين الخط واللون، يحدّد هذا الملف أيضًا حجم الصور بتحديد عرض أقصى يبلغ 80٪. هذا سيمنعها من شغل مساحة في الصفحة أكبر مما نريد. احفظ الملف وأغلقه عند الانتهاء. بعد إنشاء ملفات التطبيق وتثبيت اعتماديات المشروع، تكون جاهزًا لتشغيل التطبيق. إذا كنت قد تابعت دليل إعداد الخادم الأولي المذكور في المتطلبات الأساسية، فسيكون لديك جدار حماية نشط يسمح فقط بحركة مرور SSH. للسماح بحركة المرور عبر المنفذ 8080، نفّذ الأمر التالي : $ sudo ufw allow 8080 لتشغيل التطبيق، تأكد من وجودك في المجلد الرئيسي للمشروع: $ cd ~/node_project ابدأ تشغيل التطبيق باستخدام node app.js: $ node app.js انتقل بمتصفّحك إلى http://your_server_ip:8080. وستظهر لك صفحة الهبوط التالية: انقر على زر الحصول على معلومات القرش Get shark info. وستظهر لك صفحة المعلومات التالية: لديك الآن تطبيق قيد التشغيل. عندما تكون جاهزًا، أوقف الخادم بكتابة CTRL+C. يمكننا الآن الانتقال إلى إنشاء ملف Dockerfile الذي سيتيح لنا إعادة إنشاء هذا التطبيق وتوسيع نطاقه حسب الرغبة. الخطوة الثالثة: كتابة ملف Dockerfile يحدد ملف Dockerfile الخاص بك ما سيتم تضمينه في حاوية التطبيق عند تنفيذه. ويتيح لك استخدام هذا الملفّ تحديد بيئة الحاوية وتجنب التناقضات مع الاعتماديات أو إصدارات وقت التشغيل. باتباع هذه الإرشادات حول إنشاء حاويات محسّنة، سنجعل صورة التطبيق أكثر فاعلية قدر الإمكان عن طريق تقليل عدد طبقات الصورة وتحديد وظيفة الصورة في غرض واحد هو إعادة إنشاء ملفات التطبيق والمحتوى الثابت. في المجلّد الرئيسي لمشروعك، أنشئ الملف Dockerfile: $ nano Dockerfile تُنشَأ صور Docker باستخدام سلسلة متتالية من الصور ذات الطبقات التي تعتمد على بعضها بعضًا. ستكون خطوتنا الأولى هي إضافة الصورة الأساسية للتطبيق والتي ستشكل نقطة انطلاق إنشاء التطبيق. لنستخدم الصورة node:10-alpine. ويمكننا الحصول على الصورة alpine من مشروع Alpine Linux لكي تساعدنا في تقليل حجم الصور لدينا. أضف تعليمات FROM التالية لتعيين الصورة الأساسية للتطبيق: الملف ‎~/node_project/Dockerfile: FROM node:10-alpine تتضمن هذه الصورة Node.js و npm. ويجب أن يبتدئ كل ملفّ Dockerfile بتعليمات FROM. بشكل افتراضي، تشتمل صورة Docker Node على مستخدم Node غير جذري يمكنك استخدامه لتجنب تشغيل حاوية التطبيق باستخدام الحساب الجذري. إنها ممارسة أمان موصى بها لتجنب تشغيل الحاويات باستخدام root وتقييد الصلاحيات داخل الحاوية في تلك المطلوبة فقط لتنفيذ عملياتها. لذلك، سوف نستخدم المجلّد الرئيسي لمستخدم Node كمجلّد العمل لتطبيقنا ونعيّنه كمستخدم داخل الحاوية. لمزيد من المعلومات حول أفضل الممارسات عند العمل مع صورة Docker Node، راجع دليل أفضل الممارسات هذا إذا كانت لغتك الانجليزية جيدة. لضبط الأذونات على شيفرة تطبيقنا في الحاوية، دعنا ننشئ المجلّد الفرعي node_modules في ‎/home/node رفقة المجلّد app. سيضمن إنشاء هذه المجلّدات أنها تتوفّر على الأذونات التي نريدها، والتي ستكون مهمة عندما ننشئ وحدات node محلية في الحاوية باستخدام npm install. بالإضافة إلى إنشاء هذه المجلّدات، سنعطي ملكيتها للمستخدم node: -الملف ‎~/node_project/Dockerfile: ... RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app عيّن بعد ذلك مجلّد العمل للتطبيق على ‎/home/node/app: الملف ‎~/node_project/Dockerfile : ... WORKDIR /home/node/app إذا لم يتم تعيين مجلّد عمل WORKDIR، فسوف يُنشئ Docker واحدًا افتراضيًا، لذلك من الجيد تعيينه بشكل صريح. بعد ذلك، انسخ ملفات package.json و package-lock.json (بالنسبة لملفات الإصدار الخامس npm فما فوق): الملف ‎~/node_project/Dockerfile: ... COPY package*.json ./ تتيح إضافة تعليمة COPY قبل تنفيذ npm install تثبيت أو نسخ شيفرة التطبيق الاستفادة من آلية التخزين المؤقت لـ Docker. في كل مرحلة من مراحل البناء، سيقوم Docker بالتحقق مما إذا كان يحتوي على طبقة تم تخزينها في ذاكرة التخزين المؤقت لتلك التعليمات المحددة. إذا غيّرنا الحزمة package.json، فسيُعاد بناء هذه الطبقة، لكن إذا لم نفعل ذلك، فسوف تسمح هذا التعليمة لـ Docker باستخدام طبقة الصورة الحالية وتخطي إعادة تثبيت وحدات node. للتأكد من أن جميع ملفات التطبيق مملوكة للمستخدم node غير الجذري، بما في ذلك محتويات المجلّد node_modules، حوّل المستخدم إلى node قبل تنفيذ npm install: الملف ‎-/node_project/Dockerfile: ... USER node بعد نسخ اعتماديات المشروع وتبديل مستخدمنا، يمكننا تنفيذ install npm: الملف ‎~/node_project/Dockerfile: ... RUN npm install بعد ذلك، انسخ شيفرة تطبيقك مع الأذونات المناسبة إلى مجلّد التطبيق على الحاوية: الملف ‎~/node_project/Dockerfile: ... COPY --chown=node:node . . سيضمن هذا أن ملفات التطبيق مملوكة من قبل مستخدم node غير الجذري. أخيرًا، اعرض المنفذ 8080 على الحاوية وابدأ تشغيل التطبيق: الملف ‎~/node_project/Dockerfile: ... EXPOSE 8080 CMD [ "node", "app.js" ] لا ينشر الأمر EXPOSE المنفذَ، ولكنه يعمل بدلاً من ذلك كوسيلة لتوثيق المنافذ على الحاوية التي سيتم نشرها في وقت التشغيل. يقوم CMD بتنفيذ الأمر لبدء التطبيق، في هذه الحالة، node app.js. لاحظ أنه يجب أن يكون هناك تعليمة CMD واحدة فقط في كل Dockerfile. إذا ضمّنت أكثر من واحدة، فستُفعّل الأخيرة فقط. هناك العديد من الأشياء التي يمكنك القيام بها باستخدام Dockerfile. للحصول على قائمة كاملة بالتعليمات، يرجى الرجوع إلى التوثيق المرجعي لملفات Dockerfile. يكون الملفّ Dockerfile الكامل على هذا النحو: الملف ‎~/node_project/Dockerfile: FROM node:10-alpine RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app WORKDIR /home/node/app COPY package*.json ./ USER node RUN npm install COPY --chown=node:node . . EXPOSE 8080 CMD [ "node", "app.js" ] احفظ الملف وأغلقه عند الانتهاء من التحرير. قبل بناء صورة التطبيق، دعنا نضيف ملف ‎.dockerignore ونظرًا لأنه يعمل بطريقة مشابهة لملف .gitignore، فإن ملف ‎.dockerignore يحدّد ماهي الملفات والمجلّدات الموجودة في مجلّد مشروعك والتي لا ينبغي نسخها إلى حاويتك. افتح الملف dockerignore.: $ nano .dockerignore أضف داخل الملف وحدات node المحلية وسجلات npm وملف Dockerfile وملف dockerignore.: الملف ‎~/node_project/.dockerignore: node_modules npm-debug.log Dockerfile .dockerignore إذا كنت تعمل باستخدام Git، فستحتاج أيضًا إلى إضافة مجلّد git. وملف gitignore. احفظ الملف وأغلقه عند الانتهاء. أنت الآن جاهز لإنشاء صورة التطبيق باستخدام الأمر docker build. سيسمح لك استخدام الراية t- مع docker build بتعليم الصورة باسم سهل التذكّر. ونظرًا لأننا سنقوم بنقل الصورة إلى Docker Hub، فلنضمّن اسم مستخدم Docker Hub الخاص بنا في العلامة. سنعلّم الصورة باستخدام nodejs-image-demo، ولكن لا تتردد في استبدالها باسم تختاره. تذكر أيضًا استبدال your_dockerhub_username باسم مستخدم Docker Hub الخاص بك: $ docker build -t your_dockerhub_username/nodejs-image-demo . تحدّد النقطة . أن موضع البناء هو المجلّد الحالي. سوف يستغرق الأمر دقيقة أو دقيقتين لبناء الصورة. بمجرد اكتمال الأمر، يمكنك التحقق من صورك: $ docker images سيظهر لك في المخرجات ما يلي: output REPOSITORY TAG IMAGE ID CREATED SIZE your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 8 seconds ago 73MB node 10-alpine f09e7c96b6de 3 weeks ago 70.7MB أصبح الآن من الممكن إنشاء حاوية بهذه الصورة باستخدام docker run. وسنضمّن ثلاث علامات مع هذا الأمر: ‎-p : تنشر هذه الراية المنفذ على الحاوية وتقدّمه كمنفذ على المضيف host. سنستخدم المنفذ 80 على المضيف، ولكن يجب ألا تتردد في تعديل هذا عند الضرورة إذا كان لديك عملية أخرى تعمل على هذا المنفذ. لمزيد من المعلومات حول كيفية عمل هذا، راجع هذه النقاش في توثيقات Docker حول ربط المنافذ. ‎-d : تشغّل هذه العلامة الحاوية في الخلفية. ‎-name : هذا يتيح لنا إعطاء الحاوية اسمًا سهل التذكر. نفّذ الأمر التالي لبناء الحاوية: docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo بمجرد تشغيل حاويتك، يمكنك فحص قائمة الحاويات قيد التشغيل باستخدام docker ps: $ docker ps سيظهر لك الإخراج التالي: Output CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e50ad27074a7 your_dockerhub_username/nodejs-image-demo "node app.js" 8 seconds ago Up 7 seconds 0.0.0.0:80->8080/tcp nodejs-image-demo بعد تشغيل حاويتك، يمكنك الآن الولوج إلى تطبيقك بالانتقال بمتصفحك إلى http://your_server_ip. ستظهر لك صفحتك الهبوط مرة أخرى: الآن وبعد بناء صورة لتطبيقك، يمكنك رفعها إلى Docker Hub للاستخدام المستقبلي. الخطوة الرابعة: استخدام مستودع للعمل بالصور برفعك لصورة التطبيق الخاصة بك إلى سجلّ مثل Docker Hub، فأنت تجعلها متاحة للاستخدام المستقبلي عندما تكون بصدد بناء حاوياتك وتوسيع نطاقها. سنوضح كيف يعمل هذا عن طريق رفع صورة التطبيق إلى مستودع ثم استخدام الصورة لإعادة إنشاء حاويتنا. الخطوة الأولى لرفع الصورة هي تسجيل الدخول إلى حساب Docker Hub الذي أنشأته في المتطلبات الأساسية: $ docker login -u your_dockerhub_username أدخل كلمة مرور حساب Docker Hub، وسيؤدي التسجيل بهذه الطريقة إلى إنشاء ملف ‎~/.docker/config.json في المجلّد الرئيسي لمستخدمك بالبيانات الاعتمادية لDocker Hub. يمكنك الآن رفع صورة التطبيق إلى Docker Hub باستخدام العلامة التي أنشأتها مسبقًا، your_dockerhub_username/nodejs-image-demo $ docker push your_dockerhub_username/nodejs-image-demo دعنا نختبر الآن أهمّية سجلّ الصور بتدمير حاوية التطبيق الحالية والصورة وإعادة بنائها باستخدام الصورة التي في مستودعنا. استعرض أولاً حاوياتك التي توجد قيد التشغيل: $ docker ps سيظهر لك الإخراج التالي: Output CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e50ad27074a7 your_dockerhub_username/nodejs-image-demo "node app.js" 3 minutes ago Up 3 minutes 0.0.0.0:80->8080/tcp nodejs-image-demo باستخدام CONTAINER ID الظاهر في المخرجات، أوقف حاوية التطبيق قيد التشغيل. تأكد من استبدال المعرّف أدناه بمعرّف الحاوية CONTAINER ID: $ docker stop e50ad27074a7 اعرض قائمة صورك باستخدام الراية ‎-a : $ docker images -a سيظهر لك الإخراج التالي مع اسم صورتك، your_dockerhub_username/nodejs-image-demo، إلى جانب صورة node وباقي الصور الأخرى الخاصة ببنائك: Output REPOSITORY TAG IMAGE ID CREATED SIZE your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 7 minutes ago 73MB <none> <none> 2e3267d9ac02 4 minutes ago 72.9MB <none> <none> 8352b41730b9 4 minutes ago 73MB <none> <none> 5d58b92823cb 4 minutes ago 73MB <none> <none> 3f1e35d7062a 4 minutes ago 73MB <none> <none> 02176311e4d0 4 minutes ago 73MB <none> <none> 8e84b33edcda 4 minutes ago 70.7MB <none> <none> 6a5ed70f86f2 4 minutes ago 70.7MB <none> <none> 776b2637d3c1 4 minutes ago 70.7MB node 10-alpine f09e7c96b6de 3 weeks ago 70.7MB احذف الحاوية المتوقفة وجميع الصور، بما في ذلك الصور غير المستخدمة أو المعلّقة، باستخدام الأمر التالي: $ docker system prune -a اكتب y في المحثّ عند الإخراج لتأكيد رغبتك في إزالة الحاوية والصور المتوقفة. وينبغي التوضيح أن هذا سيؤدي أيضًا إلى حذف ذاكرة التخزين المؤقت للبناء. لقد حذفت الآن كلًا من الحاوية التي تشغّل صورة التطبيق والصورة نفسها. لمزيد من المعلومات حول إزالة حاويات Docker والصور، يرجى الاطلاع على كيفية التعامل مع الحاويات. بعد حذف كل الصور والحاويات الخاصة بك ، يمكنك الآن سحب صورة التطبيق من Docker Hub: $ docker pull your_dockerhub_username/nodejs-image-demo اعرض قائمة صورك مرة أخرى: $ docker images ستظهر لك صورة تطبيقك: Output REPOSITORY TAG IMAGE ID CREATED SIZE your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 11 minutes ago 73MB يمكنك الآن إعادة بناء حاويتك باستخدام الأمر التالي من الخطوة 3: docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo اعرض قائمة الحاويات قيد التشغيل: $ docker ps Output CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f6bc2f50dff6 your_dockerhub_username/nodejs-image-demo "node app.js" 4 seconds ago Up 3 seconds 0.0.0.0:80->8080/tcp nodejs-image-demo تصفّح http://your_server_ip مرة أخرى لعرض التطبيق قيد التشغيل. الخلاصة في هذا الدرس، عملت على بناء تطبيق ويب ثابت باستخدام Express و Bootstrap، بالإضافة إلى صورة Docker لهذا التطبيق. استخدمت هذه الصورة لإنشاء حاوية ورفعت الصورة إلى Docker Hub. بعد ذلك، تمكنت من تدمير صورتك وحاوياتك وإعادة إنشائها باستخدام مستودع Docker Hub. ترجمة -وبتصرف- للمقال How To Build a Node.js Application with Docker لصاحبته Kathleen Juell
  4. سيكون من الجيد أن نحدّ من قدرة اللاعب على التحرك للأعلى إلّا من خلال القفز عبر الفجوات أو على الصناديق. ليست هذه هي الخيارات الوحيدة المتاحة. لا يزال يتعين علينا معرفة المزيد عن المصاعد والأدراج والسلالم. لذلك، دعنا نُنشئ سُلّمًا! إليك ما سنعمل عليه في هذه المرحلة: إنشاء أول سلّم السماح للاعبين بتسلق السلالم السماح للاعبين بالوقوف والقفز على السلالم إنشاء أول سلّم لنبدأ إنشاء سلّمنا بنسخ صنف الصندوق Box: class Ladder { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; } animate(state) { if (this.sprite) { this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } } export default Ladder; import Ladder from "./ladder"; game.addObject( new Ladder( new PIXI.Sprite.fromImage( "ladder.png" ), new PIXI.Rectangle( 200, window.innerHeight - 192, 64, 192 ) ) ); game.addObject( new Box( new PIXI.Sprite.fromImage( "box.png" ), new PIXI.Rectangle( 136, window.innerHeight - 191, 64, 64 ) ) ); ستحتاج إلى إنشاء صورة ladder.png للسلم؛ بالنسبة لي، اخترت استخدام شكلٍ بسيطٍ بمقاسات 64x192 بيكسيل. تأكد من إضافة الصورة إلى اللعبة قبل إضافة اللاعب وإلا سيكون الشكل الخاص بالسلم أمام الشكل الخاص اللاعب. ستلاحظ أن اللاعب يصطدم بالسلم، كما لو كان صندوقًا. سنحتاج إلى إضافة بعض الجوالب إلى الصناديق والسلالم حتى يستطيع كشف التصادمات من تحديد ما إذا كانوا ستستمر بالاصطدام مع بعضها بعضًا … if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (object.collides && this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; this.grounded = true; this.jumping = false; return; } if (object.collides && this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (object.collides && this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (object.collides && this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } } نعطي للصندوق و السلم خاصية الاصطدام بحيث يتمكّن اللاعب من التحرّك في تجاهلٍ للاصطدامات بالكائنات التي يجب ألا يصطدم بها اللاعبون. إذا كنا سنسمح بوجود عدة لاعبين في نفس اللعبة أو المستوى، فسنضيف أيضًا خاصية الاصطدام إلى اللاعب. هذا ما لم نكن نرغب في تصادم لاعبين متعددين مع بعضهم بعضًا. السماح للاعبين بتسلق السلالم لكي يصعد اللاعب السلالم، يجب أن نكون قادرين على معرفة ما إذا كان يحاول تسلق أحدها في لحظة ما. يتعين علينا أيضًا تعليق الجاذبية والحركة الجانبية حتى لا يسقط أو ينزلق: class Player { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.velocityY = 0; this.maximumVelocityY = 20; this.accelerationY = 5; this.jumpVelocity = -40; this.climbingSpeed = 10; } animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); var onLadder = false; state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (object.collides && this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; this.grounded = true; this.jumping = false; return; } if (object.collides && this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (object.collides && this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (object.collides && this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } if (object.constructor.name === "Ladder") { onLadder = true; if (state.keys[38] || state.keys[40]) { this.grounded = false; this.jumping = false; this.climbing = true; this.velocityY = 0; this.velocityX = 0; } if (state.keys[38]) { this.rectangle.y -= this.climbingSpeed; } if (state.keys[40] && me.y + me.height < you.y + you.height) { this.rectangle.y += this.climbingSpeed; } } } }); if (!onLadder) { this.climbing = false; } if (state.keys[38] && this.grounded) { this.velocityY = this.jumpVelocity; this.jumping = true; this.grounded = false; } this.rectangle.x += this.velocityX; if (!this.climbing) { this.rectangle.y += this.velocityY; } this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } export default Player; نبدأ بإضافة خاصية جديدة سنسميها التسلق (climbing). ستكون قيمتها صحيحة "true" عندما يكون اللاعب في وضع تسلق السلم. ننشِئ أيضًا متغيرًا onLadder محلي، حتى نتمكن من معرفة ما إذا كان لا يزال واقفًا على سلم. أثناء كشف التصادمات المعتاد، نلاحظ ما إذا كان الكائن الذي يصطدم به اللاعب هو سلم أم لا. إذا كان الأمر كذلك، وكان السهم مضغوطًا لأعلى، نبدأ في التسلق. تبدأ عملية التسلق فقط إذا كان السهم مضغوطًا لأعلى، ولهذا السبب نحتاج إلى هذا المتغير المحلي. نعيد ضبط سرعة اللاعب والخصائص المتعلقة بالقفز. ونغير كذلك بشكل مباشر مستطيلَ اللاعب. إذا تم الضغط على السهم للأعلى، فإنّنا نرفع اللاعب لأعلى. إذا تم الضغط لأسفل، فإنّنا نحرّك اللاعب لأسفل حتى يصبح الجزء السفلي من اللاعب أدنى من الجزء السفلي للسلم. هذا يجعل السلالم السفلية لا تتيح للاعب أن يتجاهل التصادم مع الأرض. السماح للاعبين بالوقوف على السلالم لا تزال هناك مشكلة في السلالم. في اللحظة التي نصل فيها إلى القمة، نسقط مرة أخرى. لقد عطلنا تصادمات السلالم، لذلك نحتاج إلى طريقة للوقوف في الجزء العلوي منها حتى نتمكن من القفز أو الانتقال إلى المنصة التالية. دعنا ننقل منطق التصادم إلى الصناديق والسلالم: class Box { collides(state) { var collides = false; state.objects.forEach((object) => { if (object.constructor.name !== "Player") { return; } let edges = this.getEdges(this, object); if (!( edges.boxTop > edges.playerBottom || edges.boxRight < edges.playerLeft || edges.boxBottom < edges.playerTop || edges.boxLeft > edges.playerRight )) { collides = true; return; } }); return collides; } getEdges(box, player) { return { "boxLeft" : box.rectangle.x, "boxRight" : box.rectangle.x + box.rectangle.width, "boxTop" : box.rectangle.y, "boxBottom" : box.rectangle.y + box.rectangle.height, "playerLeft" : player.rectangle.x, "playerRight" : player.rectangle.x + player.rectangle.width, "playerTop" : player.rectangle.y, "playerBottom" : player.rectangle.y + player.rectangle.height }; } collidesInDirection(box, player) { let edges = this.getEdges(box, player); let offsetLeft = edges.playerRight - edges.boxLeft; let offsetRight = edges.boxRight - edges.playerLeft; let offsetTop = edges.playerBottom - edges.boxTop; let offsetBottom = edges.boxBottom - edges.playerTop; if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetTop) { return "↓"; } if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetBottom) { return "↑"; } if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetLeft) { return "→"; } if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetRight) { return "←"; } return "unknown"; } constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; } animate(state) { if (this.sprite) { this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } } export default Box; يستطيع صنف الصندوق Box الآن حساب ما إذا كان سيتصادم مع اللاعب. بمجرد اكتشاف التصادم بين لاعب وصندوق، يمكننا تنفيذ الدالّة التابع collidesInDirection. سنحصل على سهمٍ أنيقٍ UTF-8 يشير إلى الاتجاه الذي كان اللاعب يتحرك فيه قبل حدوث التصادم. أثناء إجراء هذا التغيير، حدث أنّ صنف السلم لا يقوم بأي شيء مختلف عن صنف الصندوق. يحدث مفهوم التسلق في فئة "اللاعب"، وبالتالي فإن فئة "السلم" يمكن أن تكون امتدادًا لفئة "الصندوق": import Box from "./box"; class Ladder extends Box { } export default Ladder; يبدو صنف اللّاعب أكثر أناقةً وتنظيمًا بعد نقل منطق التصادم خارجه: animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); var onLadder = false; state.objects.forEach((object) => { if (object === this) { return; } if (object.collides(state)) { let type = object.constructor.name; let direction = object.collidesInDirection(object, this); if (type === "Ladder") { onLadder = true; let player = this.rectangle; let ladder = object.rectangle; if (state.keys[38] || state.keys[40]) { this.grounded = false; this.jumping = false; this.climbing = true; this.velocityY = 0; this.velocityX = 0; } if (state.keys[38]) { let limit = ladder.y - this.rectangle.height + 1; this.rectangle.y = Math.max( this.rectangle.y -= this.climbingSpeed, limit ); if (player.y === limit) { this.grounded = true; this.jumping = false; } } if (state.keys[40] && player.y + player.height < ladder.y + ladder.height) { this.rectangle.y += this.climbingSpeed; } return; } if (type === "Box") { if (direction === "↓") { this.velocityY = 0; this.grounded = true; this.jumping = false; } if (direction === "↑") { this.velocityY = this.accelerationY; } if (direction === "←" && this.velocityX < 0) { this.velocityX = 0; } if (direction === "→" && this.velocityX > 0) { this.velocityX = 0; } } } }); if (!onLadder) { this.climbing = false; } if (state.keys[38] && this.grounded && !this.climbing) { this.velocityY = this.jumpVelocity; this.jumping = true; this.grounded = false; } this.rectangle.x += this.velocityX; if (!this.climbing) { this.rectangle.y += this.velocityY; } this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } لم يتغير مفهوم السلم على الأغلب، باستثناء أن اللاعب لن يصل أبدًا إلى أعلى بيكسل في أعلى درجات السلم. هكذا نمنع اللاعب من السقوط بمجرد وصوله إلى القمة. هذا يعني أنك ستحتاج إلى جعل بداية سلالمك في البيكسل الأوّل فوق المنصة التي تتوقع أن يتحرّك عليها اللاعب. وهذا يعني أيضًا أنه لا يمكن للاعب القفز من السلالم. يبدو الآن منطق التصادم المتعلّق بالصندوق أنقى وأبسط بكثير! وماهي النتيجة؟ سلالمٌ تعمل بشكل جيد. ختام اللعبة: ماذا بعد؟ لقد علمتني هذه المقالات القليلة الكثير من الأمور، أو لِنَقُل أنّهما أمران بارزان على وجه التحديد. الأول هو أن هناك قدرًا لا حصر له من الأشياء التي يمكننا إضافتها أو تغييرها في ألعابنا. والثاني هو أن هذا النشاط مربحٌ ومتعبٌ في الوقت ذاته. لقد انطلقت من رغبةٍ في معرفة كيفية صنع لعبة منصة بسيطة، وبعدها قمنا معا بتغطية كل شيء تقريبا نحتاجه لبناء تلك اللعبة. لا أريد التوقف هنا، رغم ذلك. أريد أن أستمر في التعلم وصقل مهاراتي. أريد أن أستمر في مشاركة تلك التجارب معك. ترجمة -وبتصرف- للمقال Ladders لصاحبه Christopher Pitt اقرأ أيضًا المقال السابق: الجاذبية مدخل إلى ألعاب المتصفح
  5. سنعمل هذه المرة على بنية الشيفرة لدينا، ونضيف الجاذبية إلى لعبتنا. ويجدر الذكر أننا قمنا بالفعل بمعظم العمل اللازم لتحقيق الجاذبية. إليك ما سنعمل عليه في هذه المرحلة: تنظيف الشيفرة الموجودة لدينا إضافة الجاذبية إلى لعبتنا السماح للاعبين بالقفز تنظيف الشيفرة الموجودة لدينا نحن بحاجة لتنظيف بعض الأشياء! دعنا في البداية نبدّل موضعي x و y إضافةً إلى العرض والارتفاع (بالنسبة للصندوق وللاعب) مع PIXI.Rectangle. إنّ لديهما هذه الخصائص، ولكنهما يتفاعلان أيضًا مع باقي عناصر PIXI بطرق مثيرة للاهتمام. class Box { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; } animate(state) { if (this.sprite) { this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } } export default Box; class Player { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; } animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; var move = true; state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.height + me.y > you.y) { if (this.velocityX < 0 && you.x <= me.x) { move = false; } if (this.velocityX > 0 && you.x >= me.x) { move = false; } } }); if (move) { this.rectangle.x += this.velocityX; } this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } export default Player; هل لاحظت هذا المقدار من الشيفرة الذي نستطيع حذفه؟ لقد أصبحت عمليات الموازنة طويلة قليلاً، لكن لا شيء يمكن إصلاحه من خلال متغير محليّ واحد أو اثنين. أدركتُ أيضًا أنه يمكننا تغيير قيم x و y الابتدائية من أجل التحريك. بعد ذلك، أرغب في تغليف (encapsulation) الحدث والعارض والمسرح في صنف (class) جديدة خاصّة باللعبة (Game): class Game { constructor() { this.state = { "keys": {}, "clicks": {}, "mouse": {}, "objects": [] }; } get stage() { if (!this._stage) { this._stage = this.newStage(); } return this._stage; } set stage(stage) { this._stage = stage; } newStage() { return new PIXI.Container(); } get renderer() { if (!this._renderer) { this._renderer = this.newRenderer(); } return this._renderer; } set renderer(renderer) { this._renderer = renderer; } newRenderer() { return new PIXI.autoDetectRenderer( window.innerWidth, window.innerHeight, this.newRendererOptions() ); } newRendererOptions() { return { "antialias": true, "autoResize": true, "transparent": true, "resolution": 2 }; } animate() { var caller = () => { requestAnimationFrame(caller); this.state.renderer = this.renderer; this.state.stage = this.stage; this.state.objects.forEach((object) => { object.animate(this.state); }) this.renderer.render(this.stage); }; caller(); return this; } addEventListenerToElement(element) { element.addEventListener("keydown", (event) => { this.state.keys[event.keyCode] = true; }); element.addEventListener("keyup", (event) => { this.state.keys[event.keyCode] = false; }); element.addEventListener("mousedown", (event) => { this.state.clicks[event.which] = { "clientX": event.clientX, "clientY": event.clientY }; }); element.addEventListener("mouseup", (event) => { this.state.clicks[event.which] = false; }); element.addEventListener("mousemove", (event) => { this.state.mouse.clientX = event.clientX; this.state.mouse.clientY = event.clientY; }); return this; } addRendererToElement(element) { element.appendChild(this.renderer.view); return this; } addObject(object) { this.state.objects.push(object); if (object.sprite) { this.stage.addChild(object.sprite); } return this; } } export default Game; لاحِظ دالّتي الجالب والضابط الخاصتين بصنفٍ ES6. إنهما مفيدتان لملء التبعيات الاختيارية حسب الحاجة. يمكننا تجاهل المصيّر والمسرح إذا كنا بحاجة إلى ذلك، ولكن لديهما قيم افتراضية معقولة أيضًا. هذا، على الأغلب، مجرد نقل للشيفرة التي كانت في main.js إلى game.js. الاختلاف الوحيد الملحوظ هو أننا لم نعد نحتاج إلى إضافة الأشكال إلى المسرح بشكل منفصل عن إضافة الكائنات إلى الحالة (state). أنا محتارٌ حول هذا الموضوع. فمن ناحية، يكون الأمر أكثر وضوحًا إذا أضفناها إلى المسرح يدويًا. ومن ناحية أخرى، هل سنضيف أشكالًا (مرتبطة بالكائنات) دون إضافة الكائنات؟ لا أعتقد ذلك. ربما سنعود ونغير ذلك. في الوقت الحالي، تبدو الشيفرة أنظف قليلاً. إذن، كيف تبدو main.js الآن؟ import Game from "./game"; import Player from "./player"; import Box from "./box"; var game = new Game(); game.addObject( new Box( new PIXI.Sprite.fromImage( "box.png" ), new PIXI.Rectangle( window.innerWidth * 1/4, 152, 64, 64 ) ) ); game.addObject( new Player( new PIXI.Sprite.fromImage( "player.png" ), new PIXI.Rectangle( window.innerWidth * 2/4, 200, 64, 64 ) ) ); game.addObject( new Box( new PIXI.Sprite.fromImage( "box.png" ), new PIXI.Rectangle( window.innerWidth * 3/4, 248, 64, 64 ) ) ); game.addEventListenerToElement(window); game.addRendererToElement(document.body); game.animate(); هذا يبدو أفضل بكثير! لدينا سيطرة كافية لتعيين نقاط الانطلاق لكل كائنٍ في اللعبة. ولكن بعد الإطار الأول، تبدأ الدالّة التابع ()animate في العمل. ستبدأ أشياء مثل الجاذبية والتصادمات في التحكم في كيفية تقدم اللعبة. لقد كان الأمر هكذا دائمًا، لذلك يبدو هذا الملف كذلك أيضًا. كان بإمكاننا أن نحصُر الأحداث والعارض في مجموعة صغيرة من العناصر. كما كنا نستطيع إضافة أي عدد من الكائنات إلى اللعبة من هنا. كل شيء آخر مرتبط باللعبة يوجد داخل فئة اللعبة. وكل شيء آخر مرتبط باللاعب أو الصندوق يوجد داخل الفئة الخاصّة به. هكذا تبدو الأمور أنيقة ومنظمة! إضافة الجاذبية إلى لعبتنا أحد الأشياء التي تجعل ألعاب المنصة ممتعةً هو وجود قدرٍ معتدلٍ من الأجسام في محيط اللعب. دعنا نضيف الجدران والأرضية: var game = new Game(); game.addObject( new Box( null, new PIXI.Rectangle( -10, 0, 10, window.innerHeight ) ) ); game.addObject( new Box( null, new PIXI.Rectangle( 0, window.innerHeight, window.innerWidth, 10 ) ) ); game.addObject( new Box( null, new PIXI.Rectangle( window.innerWidth, 0, 10, window.innerHeight ) ) ); game.addObject( new Player( new PIXI.Sprite.fromImage( "player.png" ), new PIXI.Rectangle( window.innerWidth * 2/4, 200, 64, 64 ) ) ); بدلاً من مربعين أحمرين، لدينا ثلاثة مستطيلات رفيعة. إنها تتصادم بنفس الطريقة. الآن، دعنا نرى كيف نضيف هذه الجاذبية: class Player { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.velocityY = 0; this.maximumVelocityY = 30; this.accelerationY = 10; this.jumpVelocity = -50; } animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; return; } if (this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } } }); this.rectangle.x += this.velocityX; this.rectangle.y += this.velocityY; this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } export default Player; نبدأ بإنشاء مجموعة من الخصائص لتتناسب مع تلك التي أنشأناها لتتبع الحركة الأفقية. لا نحتاج إلى احتكاك عمودي، إذ أنّ هذا المستوى من التفاصيل يُحذَف غالبا من ألعاب المنصّات. علينا أيضًا أن نتتبع التصادمات العمودية والأفقية. عندما يكون التصادم بين اللاعب والمنصة أو الأرضية، فإننا نوقف حركة الهبوط. وعندما يكون مع السقف، فإننا نستبدل الحركة الصعودية بقوة الجاذبية. السماح للاعبين بالقفز القفز هو ببساطة مقاومة للجاذبية لفترة قصيرة: animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; this.grounded = true; this.jumping = false; return; } if (this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } } }); if (state.keys[32] && !this.jumping && this.grounded) { this.velocityY = this.jumpVelocity; this.jumping = true; this.grounded = false; } this.rectangle.x += this.velocityX; this.rectangle.y += this.velocityY; this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } لقد قمنا هنا بتعيين مفتاح المسافة من أجل حركة القفز. نضيف فحص لوحة المفاتيح بعد فحص التصادم لأننا نريد لاعبنا أن يقفز فقط إذا كان يقف على منصة أو أرضية. يمكنك الآن إنشاء مستوياتٍ انطلاقًا من الصناديق. تستطيع كذلك إعطاءها بنيةً مرئيةً، والقفز من حولها. تمتع بقضاء بعض الوقت في إنشاء المستوى والقفز من خلاله! ترجمة -وبتصرف- للمقال Gravity لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: إنشاء السلالم وختام اللعبة المقال السابق: كشف التصادمات
  6. ألعاب المتصفح

    لقد حان الوقت لكي نتحدث عن كشف التصادمات (Detection Collision). إنه يؤثر على الأجزاء الظاهرة في لعبتنا، مثل الجدران التي لا يمكننا المرور عبرها ومثل الأرضيات التي لا يمكننا السقوط من خلالها. كما أنه يؤثر على الأجزاء المخفية في لعبتنا مثل قذائف الأسلحة ونقاط التفتيش. لن نصل إلى مفهوم الجاذبية بعد، ولن ننظر إلى صحة اللاعب أو استرداد حياته (الدم والأرواح). تعدّ الأرضيات والقذائف ونقاط التفتيش مثيرة للاهتمام، ولكنها تستحق أقسامًا خاصة بها. سنعمل على إنشاء كائنات غير نافذة (لا يمكن العبور من خلالها). سنتعلم طرقًا لمعرفة ما إذا كان جسمان يشغلان نفس الحيّز المكانيِّ. لقد قضيت بعض الوقت أبحث في هذا الموضوع. يبدو أن هناك العديد من الطرق لحل مشكلة جسمين يشغلان نفس المكان. بعض هذه الطرائق يسهل شرحها وتنفيذها وهي التي سنلقي نظرة عليها، والطرائق الأخرى ليست سهلةً ولا تزال حديثة العهد رغم ذلك. إليك ما سنعمل عليه في هذه المرحلة: *إنشاء أول صنف (class) غير متعلقة باللاعب. الكشف عن التصادمات المتعلقة بالدائرة. الكشف عن التصادمات المتعلقة بالمستطيل. إنشاء صناديق اللاعب هو كائنٌ من العديد من الكائنات التي سوف تتواجد على الشاشة في آنٍ واحدٍ. ولعبتنا هذه هي لعبة منصة، لذلك يمكننا أن نتوقع منصة واحدة على الأقل على الشاشة. لدى المنصات بعض الخصائص المثيرة للاهتمام. فهي تسمح في بعض الأحيان للاعبين بالسقوط من خلالها كما هو الحال عندما تكون واقفًا على منصة وترجع للخلف وتقفز في الوقت ذاته. تأخذ بعض الألعاب هذه السلسلة على أنك تريد السقوط من خلال المنصّة. تسمح بعض الألعاب للاعبين بالقفز في أسفل المنصات. وهذا يتيح الحركة العمودية دون وجود ثغرات في المنصات العلوية. وفي بعض الأحيان، تكون المنصات متحرّكة! تعتبر المنصّات فريدةً جدًا إلى حدّ أننا سنقضي بضعة أقسام في تنفيذ سلوكياتها المختلفة فقط. سوف نركز الآن على كائن مشترك آخر هو الصندوق الشامل (generic box). فكّر في هذا الصّندوق كمولّد للمنصّة. قد يشترك في بعض وظائفه مع المنصّة، ولكن السبب الرئيسي لوجوده هو الأشياء التي تتصادم معها وبالخصوص الكائنات مثل اللاعب. قد لا تبدو الصناديق التي سنُنشئها مثل الصناديق الحقيقية. عندما نصل إلى تطبيق الجاذبية، سنحتاج إلى صندوق عريضٍ ورفيعٍ لمنع اللاعب من السقوط خارج محيط اللعبة. وسنحتاج إلى صناديق طويلة ورفيعة لمنع اللاعب من الركض على جوانب أرضية اللعبة. سنصنع منها الجدران. وقد نصنع منها أيضًا صناديق فعلية كبيرة وخشبية تصلح للقفز عليها للوصول إلى أشياء أعلى. حسنًا، لقد تحدّثت بما يكفي. class Box { constructor(sprite, x, y, width, height) { this.sprite = sprite; this.x = x; this.y = y; this.width = width; this.height = height; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { this.sprite.x = this.x; this.sprite.y = this.y; } } export default Box; لإنشاء هذا الصّنف، نسخت ولصقت فئة "اللّاعب" وحذفت مجموعة من الأشياء. لقد كان عليّ أن أضيف إليها كذلك خاصيتي العرض والارتفاع. سوف نصل إلى ذلك بعد قليلٍ. بعد ذلك، نحتاج إلى إضافة صندوقين إلى مسرح اللّعبة: var player_sprite = new PIXI.Sprite.fromImage( "player.png" ); var player = new Player( player_sprite, window.innerWidth * 2/4, 200, 64, 64 ); var barrel_sprite1 = new PIXI.Sprite.fromImage( "barrel.png" ); var barrel1 = new Box( barrel_sprite1, window.innerWidth * 1/4, 152, 64, 64 ); var barrel_sprite2 = new PIXI.Sprite.fromImage( "barrel.png" ); var barrel2 = new Box( barrel_sprite2, window.innerWidth * 3/4, 248, 64, 64 ); stage.addChild(player_sprite); stage.addChild(barrel_sprite1); stage.addChild(barrel_sprite2); var state = { "renderer": renderer, "stage": stage, "keys": {}, "clicks": {}, "mouse": {}, "objects": [ player, barrel1, barrel2 ] }; هذا غريب، أليس كذلك! لقد أنشأتُ نسختين جديدتين من الصّندوق، وأطلقت عليهما اسم براميل. ذلك لأننا على وشك أن نلقي نظرة على التصادمات المتعلقة بالدائرة. كشف التصادمات المتعلقة بالدائرة أريدك أن تنشئ دوائر لهذه الصناديق القليلة الأولى لأننا سندرس أولاً كشف التصادمات المتعلقة بالدوائر رغم أنّ الصندوق يتميّز بالعرض والارتفاع بدلاً من الشّعاع الذي يُميّز الدائرة. ولكن، لن نستخدم هذا النوع من الكشف عن التصادمات في كثير من الأحيان ما لم تكن في منصة اللعبة الكثير من الدوائر. لنر كيف يعمل هذا النوع من الكشف: class Player { constructor(sprite, x, y, width, height) { this.sprite = sprite; this.x = x; this.y = y; this.width = width; this.height = height; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { if (state.keys[37]) { // left this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { // right this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; var move = true; state.objects.forEach((object) => { if (object === this) { return; } var deltaX = this.x - object.x; var deltaY = this.y - object.y; var distance = Math.sqrt( deltaX * deltaX + deltaY * deltaY ); if (distance < this.width / 2 + object.width / 2) { if (this.velocityX < 0 && object.x <= this.x) { move = false; } if (this.velocityX > 0 && object.x >= this.x) { move = false; } } }); if (move) { this.x += this.velocityX; } this.sprite.x = this.x; this.sprite.y = this.y; } } export default Player; أول شيء يتعين علينا القيام به هو تحديد العرض والارتفاع. رغم أننا ندعي أن اللاعبين والصناديق عبارة عن دوائر، فإننا نحتاج فقط إلى نصف العرض كشعاع للدائرة. بعد ذلك، نتحقق من كل كائن في الطبقة (state). يمكننا تجاهل اللاعب لأننا لسنا بحاجة لمعرفة متى يصطدم شيء ما بنفسه. ولكن سيكون علينا، مع ذلك، أن نتحقق من كلّ الكائنات الأخرى. تصطدم دائرتان عندما تكون المسافة بين مركزيهما أقلّ من مجموع شعاعيهما معًا. تكون نقطتاهما الأصليتان متقاربتان جدًا بحيث تتداخل خطوطهما. نفحص سريعًا لمعرفة ما إذا كان الاتجاه الذي يتحرك فيه اللاعب هو حيث يتواجد الصندوق. إذا كان الأمر كذلك، فإننا نمنع اللاعب من التحرك في هذا الاتجاه. جرّبها، فمن الممتع جدًا أن ترى كيف تعيق الأشكال غير المربّعة بعضها بعضًا. بالطبع يجب أن تكون جميعها دوائر مثالية حتى تعمل هذه الخوارزمية البسيطة. كشف التصادمات المتعلّقة بالمستطيل يكون كشف التصادمات بالنسبة للمستطيلات سهلاً مثل الدوائر. امضِ قُدُمًا واستبدل صورة البرميل بصورة صندوق. يمكنك حتى ضبط شيفرة bootstrap لتعكس شكلا مربّعًا للصناديق. هذه المرة، سنتعامل مع اللاعب باعتباره مستطيلًا. بدلاً من استعمال الشّعاع، سنحتاج إلى التحقق مما إذا كانت هناك فراغات بين المستطيل الذي يمثّل اللاعب وأيّ صندوق. نحن نسمي هذا الكشف عن التصادم في المربع المحيط المحاذي للمحور (axis-aligned bounding box) أو AABB للاختصار. إذا لم يكن هناك فراغ، وكان اللاعب يريد التحرك في اتجاه الصندوق، فإننا نمنع حدوث ذلك: var move = true; state.objects.forEach((object) => { if (object === this) { return; } if (this.x < object.x + object.width && this.x + this.width > object.x && this.y < object.y + object.height && this.y + this.height > object.y) { if (this.velocityX < 0 && object.x <= this.x) { move = false; } if (this.velocityX > 0 && object.x >= this.x) { move = false; } } }); if (move) { this.x += this.velocityX; } هذه بعض الطرق البسيطة للكشف عن التصادمات، ولكن هناك طرق أخرى. هناك طريقة تعتمد على الإسقاط في الرياضيات المتجهية لتحديد التداخل. وهناك طريقة أخرى تفحص كل خطّ في زوجٍ من المضلّعات لمعرفة ما إذا كان هناك تقاطع للخطوط. إنّها طريقة غريبة. ويمكنك كذلك تجربة مجموعات من الدوائر تتصادم معًا. قد يكون الأمر ممتعًا. سوف أشغّل قليلاً هذه الدائرة الخضراء الصغيرة لتتحرّك نحو هذه الصناديق الحمراء الصغيرة. نلتقي فيما بعد… ترجمة -وبتصرف- للمقال Collision Detection لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: الجاذبية المقال السابق: جلب المدخلات من اللاعب
  7. ما هو الفرق بين الفيلم واللعبة؟ إنّه يكمن في إمكانية جلب المدخلات من اللاعب! في الواقع، من أهمّ الأشياء في تصميم الألعاب هو أنّه في كثير من الأحيان تُعرّف اللعبة من خلال كيفية استقبالها لمدخلات اللاعب. تعتمد ألعاب السباق على إدخال ثابت ودقيق. إذا رفعت أصابعك عن لوحة المفاتيح، أو رفعت قدمك عن الدواسة، فستفقد قوة الدّفع. وإذا لم تدخل تجميعة المفاتيح الصحيحة، فسوف تتعطل عملية القيادة. تحتاج ألعاب المنصّات platform games (مثل اللعبة التي نُنشئها) إلى إدخالٍ من كلتا اليدين. غالبًا ما يكون الجانب الأيمن من لوحة المفاتيح مخصّصًا للحركة. أما الجانب الأيسر من لوحة المفاتيح فيكون غالبًا مخصّصًا لإجراءات اللاعب، مثل إطلاق النار أو فتح الأبواب. ربما ترغب في تصميم لعبة منصة مثل Terraria، والتي تستخدم الفأرة لإجراءات اللاعب والتصويب على الهدف. ربما تريد استخدام WASD لتحريك اللاعب على الشاشة. هيا بنا إلى العمل! إليك ما سنعمل عليه في هذه المرحلة: نقل صنف اللاعب إلى وحدة نمطية الكشف عن الأحداث الخاصّة بلوحة المفاتيح والفأرة التسارع والتباطؤ إنشاء الوحدات لقد أنشأنا سابقًا صنف اللاعب وجعلناها مسؤولة عن حركة اللاعب. ولكننا تركناها في ملف جافاسكربت الرئيسي، وهذا سيجعل الأمر فوضويًا عندما يزداد عدد الفئات لدينا. لذلك سنُنشئ لها وحدة خاصّة: class Player { constructor(sprite, x, y) { this.sprite = sprite; this.x = x; this.y = y; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { this.x += 5; if (this.x > window.innerWidth) { this.x = 0; } this.sprite.x = this.x; this.sprite.y = this.y; } } export default Player; يمكن أن تلاحظ أنّ فئة اللاعب مماثلة لتلك التي أنشأناها من قبل. الجزء المهم هنا هو export default Player. هذا يجعل فئة اللاعب قابلة للاستيراد بالطريقة التالية: import Player from "./player"; var player = new Player( sprite, window.innerWidth / 2, window.innerHeight / 2 ); هذا أسلوب شائع لتقسيم الشيفرة، وغالبًا ما ينقل الفئات الفردية إلى ملفات خاصة بها. إنه يحافظ على بساطة الأشياء، ويُكسِبنا القدرة على إعادة استخدام الشيفرة بدون نسخ ولصق. لا تنس أن تتحقق من أن الشّكل ما زال يتحرك عبر الشاشة. ومثلما ذكرنا من قبل، يمكنك بدء اختبارٍ للخادم باستعمال php -S 127.0.0.1:8000. الكشف عن المدخلات عندما حاولت كشف المدخلات لأول مرة، حاولت إضافة مُستمعات الأحداث إلى جميع الكائنات. حاولت إضافة مستمعات للاعب وأخرى للمسرح وهذا أخرج الأمور عن السيطرة. تكمن الحيلة في كشف الأحداث في المستوى العلوي، وحفظ تفاصيلها في كائن خاصّ بحالة اللعبة (game state object). لقد أنشأنا من قبل هذا النوع من الكائنات الخاصّ بحالة اللعبة، وقمنا بتمريره إلى الدالة الوظيفية animate()‎ الخاصّة بتحريك اللاعب. دعنا نتوسع في هذه الحالة: الملف main.js: var state = { "renderer": renderer, "stage": stage, "keys": {}, "clicks": {}, "mouse": {} }; window.addEventListener("keydown", function(event) { state.keys[event.keyCode] = true; }); window.addEventListener("keyup", function(event) { state.keys[event.keyCode] = false; }); window.addEventListener("mousedown", function(event) { state.clicks[event.which] = { "clientX": event.clientX, "clientY": event.clientY }; }); window.addEventListener("mouseup", function(event) { state.clicks[event.which] = false; }); window.addEventListener("mousemove", function(event) { state.mouse.clientX = event.clientX; state.mouse.clientY = event.clientY; }); animate(); function animate() { requestAnimationFrame(animate); player.animate(state); renderer.render(stage); } بدأنا بإضافة المصيّر والحالة إلى كائن حالة اللعبة. لقد أضفنا الآن المفاتيح والنقرات وخصائص الفأرة والتي تتعقب الأزرار والحركات. بينما نضيف المزيد من الكائنات إلى اللعبة، سنمرّر كائن حالة اللعبة إلى كل منها. ويمكنها استخدامه لتعرف ما يجب عليها فعله عندما يتفاعل اللاعب مع اللعبة. دعنا نوقف حركة اللاعب الحالية ونضيف حركةً تعتمد على المُدخلات: animate(state) { if (state.keys[37]) { // left يسار this.x = Math.max( 0, this.x – 5 ); } if (state.keys[39]) { // right يمين this.x = Math.min( window.innerWidth - 64, this.x + 5 ); } if (state.clicks[1]) { // left click نقرة يسار this.x = state.clicks[1].clientX; } this.sprite.x = this.x; this.sprite.y = this.y; } هناك تغيير فعلي بسيط في الدالّة ()animate. نترقب المفتاحين السهميين، ونتحرك إذا ضغط اللاعب على أحدهما. نتحقق أيضًا مما إذا نقر اللاعب على الفأرة، ونحرّك اللاعب على الفور إذا كان الأمر كذلك. حركة اللاعب الطبيعية يستطيع اللاعب التحرك الآن قليلاً. لكنه لا يحسّ أن الأمور جيدة رغم ذلك. في اللحظة التي نترك فيها مفتاح السهم، فإنه يتوقف وحسب. كما أنّ سرعته تبقى على نسق واحد هو النسق البطيئٍ. ما نحتاج إليه هو بعض التسارع للسماح للاعب بتسريع حركته في اتجاهٍ ما. يمكننا أيضًا استخدام بعض الاحتكاك لإبطاء اللاعب عندما لا نرغب في التسارع. دعنا نضيف هذه الأمور: class Player { constructor(sprite, x, y) { this.sprite = sprite; this.x = x; this.y = y; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { if (state.keys[37]) { // left this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { // right this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.x += this.velocityX; this.sprite.x = this.x; this.sprite.y = this.y; } } هيّا بنا! دعنا نراجع هذا في بضع خطوات: لقد أنشأنا مجموعة من الخصائص: السرعة والسرعة القصوى والتسارع والاحتكاك. لقد وضعناها على قيمٍ افتراضية معقولة. لا تتردد في تجربة قيمها حتى تتأكد فعلًا أنها كذلك. يمكننا أن نبدأ في تتبع تسارع لاعبنا في أي من الاتجاهين. إذا ضغطنا على المفتاح الأيسر، فسنبدأ بتسريع اللاعب في هذا الاتجاه. هناك تسارع أعظمي يمكن أن يصل إليه اللاعب، لذلك لا يتجاوزه. في غياب نوع من القوة المضادة، سيستمر اللاعب في التحرك في نفس الاتجاه. هذا يشبه ما يحدث في الفضاء حيث لا توجد مقاومة الهواء أو الجاذبية لمواجهة القوّة الدافعة. نعرّف هذه القوة المضادة على أنها احتكاك، ونضاعف بها السرعة. إذا لم يكن هناك تسارع، فهذا يعني أن القوّة الدافعة تؤول نحو الصفر. إنه يعطي انطباعًا أن اللاعب قد توقف. ترجمة -وبتصرف- للمقال Player Input لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: كشف التصادمات المقال السابق: حلقة اللعبة التكرارية
  8. الحلقات التكرارية هي جزء أساسي من كلّ لعبة. سنهيّئ في هذا المقال المسرح للعبتنا من خلال إنشاء مخطّط قويٍّ لسير العمل وبيئة متينة كذلك. سنطّلع على بعض المكتبات المفيدة، كما سننشئ أوّل شخصية لنا في اللعبة. إنه لأمرٌ ممتعٌ! إليك ما سنعمل عليه في هذه المرحلة: تهييئ مسرح اللعبة إنشاء وعرض الأشكال فهم وبناء حلقات اللعبة تهييئ مسرح اللعبة إن هناك الكثير من الأشياء التي ينبغي التفكير فيها عند إنشاء لعبة قائمة على المتصفح. سنتعلم التقنيات التي تعمل مع العديد من المنصات واللغات المختلفة. سنستخدم أيضًا مُحرّك عرضٍ ثنائي الأبعاد يسمى Pixi.js. إنّه ليس مُحرّك ألعاب أو مُحرّك أجسام مادّية. ربّما لا يزال يتعين علينا أن نعرف أكثر كيف تعمل الأجسام المادّية ومكوّنات اللّعبة، ولكن على الأقل لن نضيع الوقت في تناقضات المتصفح. يمكننا أن نأخذ هذا الخيار عندما لا يكون لدينا ما هو أفضل للقيام به. في الوقت الحالي، سنخلق بيئة للمحرّك Pixi كي يعمل فيها: { "scripts": { "build": "browserify main.js -t babelify --outfile main.dist.js", "watch": "watchify main.js -t babelify --outfile main.dist.js" }, "dependencies": { "pixi.js": "^3.0" }, "devDependencies": { "babel": "^5.4", "babelify": "^6.1", "browserify": "^10.2", "watchify": "^3.2" } } أريد أن أكون قادرًا على استخدام ميزات ES6. لذلك، سنحوّل شيفرة ES6 إلى شيفرة ES5 احترازًا حتى يشمل الدعم المتصفحات القديمة. الأدوات التي سنستخدمها في ذلك هي Babel و Browserify و Watchify. سوف يترجم Babel شيفرة ES6 إلى شيفرة ES5. أمّا Browserify فسيربط جميع وحدات ES6 الخاصة بنا أي ملفّاتنا جافاسكربت فيما بينها، وذلك باستخدام صيغة الاستيراد (import) في حين سيعمل Watchify على تفعيل كلّ من Babel وBrowserify عندما تتغير ملفاتنا. سنستعمل أيضًا الإصدار 3.0 من Pixi كما سنستعمل سكريبتين NPM سيكونان مجرّد اختصار للأوامر التي قد نُدخلها في الطّرفيّة. وهكذا، بدلاً من كتابة browserify ... main.dist.js نكتب فقط npm run build. <!doctype html> <html> <head> <style> html, body { margin: 0; padding: 0; cursor: default; background: #000; overflow: hidden; } </style> </head> <body> <script src="node_modules/pixi.js/bin/pixi.js"></script> <script src="main.dist.js"></script> </body> </html> قد تتساءل هل سنحتاج إلى تضمين كلّ ملفّ جافاسكربت أنشأناه. كلّا، لأن Browserify يربط جميع ملفات جافاسكربت التي تُستورَد إلى main.js فيما بينها ثم يُحوّلها بواسطة Babel، مما يؤدي إلى ملفٍّ واحدٍ. سنحتاج فقط لاستيراد هذا الملف. ورّبما تتساءل أيضًا عن العناصر التي سنعرضها في لعبتنا. ستندمج الشيفرة التي نكتبها لعرض لعبتنا تلقائيًا في نص الملفّ. يمكنك عرض هذه الملفات بالانتقال إلى مجلّدها في الطّرفية، وتنفيذ الأمر التالي: php -S 127.0.0.1:8000. يشغّل هذا الأمر خادمَ اختبارٍ PHP في عنوان URL يمكنك وضعه في متصفحك. ستحتاج إلى ذلك قبل أن تتمكن من تحميل صور الأشكال، لذا جرِّب ذلك الآن. إنشاء الأشكال الأشكال اسمٌ شائعٌ للكائنات المرئية في اللعبة. نستطيع تحريكها على شاشة اللعبة رغم أنها غالباً ما تكون مسؤولة عن تحريك نفسها، كما نستطيع التفاعل معها كذلك. ماريو هو أحد هذه الأشكال، والمنصّات التي يمشي عليها أشكالٌ أيضًا، كما أنّ الغيوم في الخلفية أشكالٌ كذلك. فكِّر في الأشكال بعدِّها شرائحَ من ملفّ التصميم الذي نرسمه بناءً على هياكل البيانات المجردة. وتمتلك هياكل البيانات المجردة هذه موقعًا في الشّاشة، وأحيانًا تكون لديها سرعةٌ، كما أنّها هي التي نُطبِّق عليها قواعد اللعبة. إنها تمثل مصادر قوّتنا وأعداءَنا. إذن كيف نُنشِئها؟ لنبدأ بإنشاء صورة PNG صغيرة. نستخدم صيغة PNG لأنها تسمح لنا بجعل أجزاء من نسيج الشّكل شفافةً. يمكنك استخدام صور JPEG لأشكالك إذا كنت ترغب بذلك. بعد ذلك، نحتاج إلى إنشاء أداةِ عرضٍ ومسرحٍ وشكلٍ من الأشكال: var renderer = new PIXI.autoDetectRenderer( window.innerWidth, window.innerHeight, { "antialias": true, "autoResize": true, "transparent": true, "resolution": 2 } ); document.body.appendChild(renderer.view); var sprite = new PIXI.Sprite.fromImage("player.png"); sprite.x = window.innerWidth / 2; sprite.y = window.innerHeight / 2; var stage = new PIXI.Container(); stage.addChild(sprite); animate(); function animate() { requestAnimationFrame(animate); renderer.render(stage); } حسنًا، تحدث أمور كثيرة هنا، دعنا نلقي نظرة على هذه الشيفرة على مراحل: 1- نُنشِئ المصيّر، وهو أداة تُحوّل العناصر المجرّدة في Pixi (أي الأشكال والأنسجة وغيرها) إلى رسومات قماشية أو رسومات WebGL وتعرضها. ليس علينا أن نتفاعل معها، ولكن يجب أن يكون لدينا دائمًا عارضٌ لعرض عناصر Pixi الخاصة بنا. نطلب من العارض أن يغطّي نافذةَ المتصفح كاملة بالطول والعرض. إننا نطلب ذلك من أجل تنقية وضوح الرسومات باستعمال مضادات التعرج (Anti Aliasing) ورفعها إلى دقة شبكية العين. نطلب ذلك أيضا للحصول على خلفية شفافة وتغيير حجم جميع الرسومات لتلائم الشاشة. 2- بعدها، نُنشِئ نسخة جديدة للفئة PIXI.Sprite، باستخدام الصورة PNG التي أنشأناها سابقًا. بشكل افتراضي، تكون النسخة في الموضع x = 0 و y = 0. ويمكننا وضعها في وسط الشاشة بدلًا من ذلك. يتعين علينا إنشاء حاوية جذرية للأشكال والتي تسمى غالبًا مسرحًا، ثم إلحاق الشّكل بالمسرح. إنّ الأمر أقل تعقيدًا مما قد يبدو عليه. فكِّر في الأمر مثل صفحات HTML. يكون لدى الصفحة دائمًا عنصرٌ جذريٌّ HTML يحتوي جميع العناصر الأخرى التي نضيف إليه. إنّه نفس الشيء هنا. 3- أخيرًا، ننشئ دالّةً للتحريك ونحرص على أن يعرض العارض المسرح والشّكل معًا. نستخدم الدالّة requestAnimationFrame()‎ كطريقة للعرض دون إعاقة سلسلة جافاسكربت. إنّ هناك نقاشًا طويلًا يمكن أن نخوضه حول هذا الموضوع. في الوقت الحالي، من المهم فقط أن تعرف أن الدالّة requestAnimationFrame()‎ تُنفّذ عدة مرات في الثانية. ويتجلى الهدف بالنسبة للعبتنا هنا في عرض ما بين 30 و 60 إطارًا في الثانية، مع القيام بكل أنواع الحسابات في الخلفية. هذه هي الدّالّة التي نحتاج إلى استخدامها حتى يسير كل شيء بسلاسة. حلقات اللعبة سوف تتحكم حلقة اللعبة في تدفق لعبتنا. إنها عملية متكررة لقراءة المُدخلات، وحساب التغييرات في الحال وتقديم المُخرجات إلى الشاشة. كلّ ما قُمنا به حتى الآن هو عرض صورة ثابتة أي إعداد السقالة فحسب. دعنا نتحرك قليلا! بادئ ذي بدء، سنُنشِئ فئة لاعبٍ، حتى نتمكن من تتبع موضع اللاعب: class Player { constructor(sprite, x, y) { this.sprite = sprite; this.x = x; this.y = y; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { this.x += 5; if (this.x > window.innerWidth) { this.x = 0; } this.sprite.x = this.x; this.sprite.y = this.y; } } هذا ما تبدو عليه أصناف جافاسكريبت في ES6. تمتلك فئة المشغل مُنشئًا (constructor) نستخدمه لتخزين مرجعٍ يحيل على الشَّكل ويخزّن كذلك إحداثيتي x و y البدئيتين، كما أنّها تمتلك كذلك دالّةً وظيفية animate()‎ سنستدعيها عدة مرات في الثانية. يمكننا استخدام الدّالة لمعرفة ما إذا كان اللاعب يحتاج إلى تغيير موضعه أو القيام بشيء آخر. في هذه الحالة، نُحرّك موضع x للشّكل قليلاً. إذا تحرك اللاعب/الشّكل من الجانب الأيمن من الشاشة، فإننا نعيده إلى الجانب الأيسر منها. ينبغي علينا أيضًا أن نغيّر كيفية إنشاء الشّكل: var sprite = new PIXI.Sprite.fromImage("player.png"); var player = new Player( sprite, window.innerWidth / 2, window.innerHeight / 2 ); stage.addChild(sprite); var state = { "renderer": renderer, "stage": stage }; animate(); function animate() { requestAnimationFrame(animate); player.animate(state); renderer.render(stage); } الآن، بدأنا نشاهد مفعول الحلقة في لعبتنا إذ نطلب من لاعبنا تحريك الشّكل عدة مرات في الثانية. يأخذ اللاعب في الاعتبار بعض منطقه الخاصّ، ويُعدّل الشّكل الخاصّ به كما يريد. يعرض العارض النتيجة في المُخرجات، وما نراه هو رسوم متحركة سلسة. الشيء الوحيد الذي ينقصنا لحدّ الآن هو الإدخال، لكننا سنصل إلى هذه المرحلة قريبًا! ترجمة -وبتصرف- للمقال The Game Loop لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: جلب المدخلات من اللاعب المقال السابق: مدخل إلى صناعة ألعاب المتصفح
  9. إنّ التكامل المستمر (Continuous integration) سهل للغاية. نزّل خدمة الأتمتة مفتوحة المصدر Jenkins، وثبّتها، وأنشئ وظيفة، وانقر على الزر، ثم احصل على رسالة بريد إلكتروني جميلة تفيد بأن إنشاءك مكسور أو معطّل (أفترض أن إنشاءك تلقائي). بعد ذلك، أصلح الاختبارات التي لا تعمل (أفترض كذلك أن لديك اختبارات)، واحصل على رسالة بريد إلكتروني أجمل تفيد أن إنشاءك على ما يرام. بعد ذلك، غرّد حول الموضوع مدعيا أن فريقك يستخدم التكامل المستمر. ثم في غضون أسابيع قليلة، ابدأ في فلترة تنبيهات Jenkins إلى مجلدها الخاص لكي لا تستمرّ بإزعاجك. على أي حال، ليس لدى فريقك الوقت أو الرغبة لإصلاح جميع اختبارات الوحدة في كل مرة يعطّلها شخص ما. بعد كل شيء، نعلم جميعًا أن اختبارات الوحدة لا تناسب فريقًا يعمل وفق مواعيد نهائية محدّدة، أليس كذلك؟ هذا خطأ. يمكن للتكامل المستمر أن يعمل في هذه الظروف بل ويجب أن يعمل فيها. ما هو التكامل المستمر؟ في الوقت الحاضر، تُطوّر البرمجيات في فرق نعمل على تطوير فروعٍ للميزات وعزل التعديلات أثناء تطويرها. ثم ندمج الفروع في فرع رئيسي master. وبعد كل عملية دمج، يُختبر المنتج بالكامل، وتُنفّذ جميع اختبارات الوحدة والتكامل المتاحة. وهذا ما يسمى «التكامل المستمر» (continuous integration ويعرف أيضا بالاختصار "CI"). في بعض الأحيان، تفشل بعض الاختبارات. عندما يحدث ذلك، نقول إن "البناءات مكسورة". مثل هذا الفشل هو أحد الآثار الجانبية الإيجابية لمراقبة الجودة لأنه يعطي تنبيهًا بعلامة حمراء فور حدوث خطأ ما. هذه ممارسة معروفة في تطوير البرمجيات، عندما يصبح إصلاح هذا الخطأ أولوية قصوى للمتسبّب فيه وللفريق بأكمله. إذ يتوجّب إصلاح الخطأ مباشرة بعد أن يرسل خادم التكامل المستمر تنبيهًا بالعلامة الحمراء. هناك بعض الأدوات الجيدة المتاحة في السوق، والتي تُؤتمت إجراءات DevOps. بعضها مفتوح المصدر يمكنك تنزيله وتثبيته على الخوادم الخاصة بك. نذكر منها على سبيل المثال: Jenkins و Go و CruiseControl. وبعضها الآخر متاح كخدمات سحابية، مثل: Travis و Shippable و Wercker وغيرها الكثير. لماذا لم يعد التكامل المستمر صالحًا؟ إن التكامل المستمر رائعٌ، ولكن في أغلب الأحيان كلما كان الفريق أكبر (وقاعدة الشيفرة كذلك)، كلما تعطلت الإنشاءات وكلما طالت المدة لإصلاحها. لقد رأيت العديد من الأمثلة يبدأ فيها فريق يعمل بجِدٍّ بتجاهل التنبيهات الحمراء، التي يرسلها Jenkins. بعد بضعة أسابيع، يصبح الفريق ببساطة غير قادر على إصلاح جميع الأخطاء في الوقت المناسب. وهذا، في الغالب، لأن العمل له أولويات أخرى. إذ لا يفهم مالكو المنتجات أهمية "البنية النظيفة" ولا يمكن للمدراء التقنيين شراء الوقت لتثبيت اختبارات الوحدة. كما أن الشيفرة المكسورة تكون أصلا في الفرع الرئيسي master، و في معظم الحالات، تكون قد نُشرت بالفعل على الإنتاج وسُلّمت للمستخدمين النهائيين. فما الحاجة إذن لإصلاح الاختبارات إذا كان العمل قد سُلّم فعليًا؟ لا تأخذ معظم فرق التطوير تنبيهات التكامل المستمر على محمل الجد. لا تمثّل Jenkins أو Travis بالنسبة لهم سوى مجرد أدوات لطيفة لا تلعب أي دور في عملية التطوير والتسليم بأكملها. بغض النظر عن ما يقوله خادم التكامل المستمر، مازلنا نقدم ميزات جديدة للمستخدمين النهائيين. ونترك إصلاح إنشائنا إلى وقت لاحق. وهذا منطقي بالنسبة لي. ما هو الحل؟ منذ بضع سنوات، نشرت مقالًا بعنوان "منع التعارضات في مشاريع PHP الموزّعة والمرنة". اقترحت في المقالة حلولًا لهذه الإشكالات في Subversion و PHP. منذ ذلك الوقت، استخدمت هذا النهج تجريبيًا في عدد من المشاريع مفتوحة المصدر وبعض المشاريع التجارية أيضا في PHP وجافا وروبي وجافاسكريبت و Git و Subversion. في جميع الحالات، كانت تجربتي إيجابية، ولهذا السبب رأى rultor.com النور لاحقًا. لذلك، فإن الحل بسيط للغاية: احظُر على أي شخص دمج أي شيء في الفرع الرئيسي master وأنشئ سكريبتًا يمكن لأي شخص استدعاؤه. سيقوم السكريبت بعمليات الدمج والاختبار والتنفيذ. لن تكون لدى السكريبت أي استثناءات. فإذا انكسر أي فرع في اختبار واحد، سيرفض الفرع بأكمله. بمعنى آخر، يجب أن تظهر تلك العلامة الحمراء للتنبيه قبل أن تنتقل الشيفرة إلى الفرع الرئيسي master. ويجب أن يتحمّل مسؤولية الاختبارات المكسورة من تسبّب فيها. لنفترض أنّني أقوم بتطوير ميزة في فرعي الخاص. انتهيت من التطوير وكسرت بعض الاختبارات عن طريق الخطأ. يحدث ذلك، فنحن جميعا نرتكب أخطاء. ولكن لا يمكنني دمج تعديلاتي في Master. سيرفض Git ببساطة تنفيذ الأمر push لأنني لا أملك الأذونات المناسبة. كل ما يمكنني فعله هو أن أستدعي السكريبت السحري، وأطلب منه دمج فرعي الخاص. سيحاول السكريبت الدمج، ولكن قبل الدخول إلى الرئيسية، ستشغّل جميع الاختبارات. وإذا كُسِر أي منها، سيكون الرفض نتيجة حتمية. لن تُدمج التعديلات التي أجريتها وأصبح لزامًا عليّ إصلاحها قبل استدعاء السكريبت مرة أخرى. قد يؤدي هذا النهج في البداية إلى إبطاء عملية التطوير، لأن على الجميع بدء كتابة تعليمات برمجية أنظف. ولكن في النهاية، فإن هذه الطريقة تؤتي ثمارها. إنشاءات ما قبل الإقلاع تقدم بعض خوادم التكامل المستمرّ CI ميزة "إنشاءات ما قبل الإقلاع"، وهذا يعني اختبار الفروع قبل دمجها في master رئيسي. لدى Travis، على سبيل المثال، هذه الميزة المفيدة للغاية. عندما تقوم بإيداع جديد في فرع ما، يحاول Travis إنشاءه على الفور، ويُبلغ عبر طلبات الإضافة في GitHub عن أي مشاكل قد تحدث. انتبه، فإنشاءات ما قبل الإقلاع لا تندمج. دورها فقط هو التحقّق مما إذا كان فرعك الخاصّ نظيفًا. بعد الدمج، يمكن بسهولة كسر الفرع الرئيسي master. لذا، فهذه الآلية لا تضمن أنه لا أحد يستطيع الإيداع بشكل مباشر، والتسبب بكسر عن طريق الخطأ. تبقى عمليات الإنشاء قبل الإقلاع تدبيرًا وقائيًا، لكنها لا تحل المشكلة تمامًا. موقع Rultor.com من أجل البدء في العمل وفق ما هو موضح أعلاه، كل ما عليك فعله هو إلغاء أذونات الكتابة في الفرع الرئيسي master (أو ‎/trunk، في Subversion). لسوء الحظ، هذا غير ممكن في GitHub. الحل الوحيد هو العمل من خلال الفروع وطلبات السحب فقط. ما عليك سوى إزالة الجميع من قائمة المتعاونين (collaborators) وسيكون عليهم إرسال تعديلاتهم من خلال طلبات السحب. بعد ذلك، ابدأ في استخدام موقع rultor.com الذي سيساعدك على اختبار كل طلب للإضافة ودمجه ودفعه. في الأساس، Rultor هو السيناريو الذي كنا نتحدث عنه أعلاه، وهو متاح كخدمة سحابية مجانية. ترجمة -وبتصرف- للمقال Master Branch Must Be Read-Only لصاحبه Yegor Bugayenko
  10. روتينات رائعة ومفيدة بالتأكيد رغم أنها تحتاج إلى قدر كبير من الالتزام. ولكن ساعة البافلوك ب 150 دولار من أجل الاستيقاظ...😥
  11. تعرف على devopssec

    بارك الله فيك شكرا لك.
  12. إنّ ما ترغب فيه عندما تحتاج إلى إنشاء منتج برمجي ولكن خبرتك في هندسة البرمجيات محدودة هو التعهيد الخارجي للبرمجيات. إنها ممارسة تجارية ذكية يستخدمها الجميع بدءًا من أصحاب الألف دولار الذين يملكون مواقع الويب الشخصية وانتهاء بأصحاب الثروات الكبيرة؛ وكلهم يفشلون إلى حد ما. في الواقع، من الصعب جدًا أن لا تفشل. لذا أعددت فيما يلي قائمة من التلميحات البسيطة لكل من يقرّر التعهيد الخارجي لتطوير البرمجيات (يوجد أهمها في الأسفل). ولكم تمنيت لو أن شخصًا ما قد أعطاها لي منذ 15 عامًا. لتكن لديك اتفاقية "العمل مقابل أجر" تأكد من أن العقد الذي أبرمته مع الفريق المُتعهِّد لإنشاء البرامج يشتمل على شيء من هذا القبيل: "يجب على الأطراف اعتبار جميع التسليمات التي أنشأها المطور بمثابة أعمال أُنجِزت للتأجير كما هو محدد بموجب قوانين حقوق الطبع والنشر". هذا هو التعريف المختصر والسهل لعبارة "كل ما تنجزه لي هو ملكي". ضع هذا في العقد ولن تستطيع شركة التعهيد الخارجي ادعاء أن البرنامج الذي أنشأته مِلكها، وهو ما يحدث هنا وهناك. احرص على امتلاك مستودعٍ خاصٍّ بك للشيفرة المصدرية تأكد من أن مستودع الشيفرة المصدرية تحت سيطرتك. وأفضل طريقة للقيام بذلك هي إنشاء مستودع GitHub خاصٍّ. سيكون المخزون مرئيًا ولا يمكن الوصول إليه إلّا من خلالك أنت وفريق التعهيد الخارجي. علاوة على ذلك، تأكد من أن الفريق لديه صلاحية القراءة فقط ولا يمكنه تغيير الشيفرة مباشرة إلا من خلال طلبات السحب (pull requests)، لأن Git يتيح إمكانية تدمير السجل بأكمله بضغطة واحدة "قسرية" على الفرع الرئيسي (master branch). لذلك سيكون من الأفضل لك أن تكون الشخص الوحيد الذي لديه صلاحية الكتابة. لجعل الأمور أكثر بساطة، أنصحك باستخدام Rultor كأداة لدمج طلبات السحب هذه بشكل شبه تلقائي. اجمع الإحصائيات بانتظام اطلب من أعضاء فريق التعهيد الخارجي جمع الاحصائيات بانتظام من البرنامج الذي يقومون بإنشائه وإرسالها إليك بطريقة أو بأخرى (عبر البريد الإلكتروني ربما). أنصح باستخدام Hits-of-Code ومدى تغطية اختبارات الوحدة (أو فقط إجمالي عدد اختبارات الوحدة) والتذاكر المفتوحة والمغلقة ومدة الإنشاء. أنا أتحدث هنا عن مقاييس عملية. وهو ما لن تحصل عليه فعليًا باستخدام أدوات أخرى مثل NewRelic. ستحدّد هذه المقاييس أداء الفريق، وليس المنتج قيد التطوير. لا أقول أنه يجب عليك إدارة الفريق من خلال المقاييس، ولكن عليك أن تراقب هذه الأرقام وديناميكيتها. إجراء مراجعات فنية مستقلة تتجلى أهمية هذه المراجعات في صعوبة تضخيمها أو التحايل فيها. إنها حاسمة بشكل استثنائي عندما يتعلق الأمر بالتعهيد الخارجي للبرمجيات. في الواقع، هذه هي الطريقة الأفضل لجمع المعلومات الموضوعية عن البرنامج الذي تحصل عليه من جهة خارجية. لا تعتمد على التقارير والوعود والضمانات والتوثيقات الشاملة. بدلاً من ذلك، استأجر شخصًا آخر بنظام الساعة واطلب منه مراجعة ما يطوّره شريك الخارجي. قم بهذ المراجعات بانتظام وبشكل منهجي. لا تخف من الإساءة إلى المبرمجين، واشرح لهم بصراحة أهمية المراجعة بالنسبة لك. إذا كانوا مهنيين، فسوف يفهمون ويحترمون أهداف عملك. يمكنك أيضا أن تريهم هذه المقالة :-). أتمتة النشر والتحكم فيه اطلب من الفريق المُتعهِّد أتمتة آلية النشر بالكامل وإبقاءها تحت سيطرتك. وأنصح بأن تفعل هذا خلال الأيام الأولى للمشروع. هذا يعني أنه ينبغي تجميع المنتج واختباره وتعبئته وتثبيته ونشره في مستودع الإنتاج (أو على الخادم/الخوادم) بنقرة واحدة. ستحتاج بالطبع إلى إنشاء سكريبت لأتمتة سلسلة العمليات هذه ويجب على شريكك الخارجي أن يكتبه لك. ثم، عند بدء عملية التطوير، يُنفّذ السكريبت تلقائيًا في كل مرة يجري فيها تعديل جديد على المستودع الذي ينشر الإنتاج. المهم هنا هو أن تعرف كيفية عمل هذا السكريبت وكيفية تشغيله حتى تكون قادرًا على بناء منتجك ونشره بنفسك. اطلب إصدارات أسبوعية لبرنامجك لا تنتظر الإصدار النهائي؛ بل اطلب من الفريق المُتعهِّد إصدار نسخة جديدة كل أسبوع. بغض النظر عن مدى كثافة التطوير وعدد الميزات الجاري تنفيذها، من الممكن دائمًا تجميع نسخة جديدة وإصدارها. إذا كان التطوير مكثفًا فعلاً، فاطلب من فريقك استخدام GitFlow أو شيء مشابه لعزل فرع الإنتاج المستقر عن باقي الفروع. لكن احذر من طول الانتظار! احرص على رؤية برنامجك مجمّعًا ومنشورًا كل أسبوع، بدون استثناءات ولا أعذار. إذا لم يتمكن الفريق المُتعهِّد من تقديم ذلك لك، فهذا يدعوك للقلق وإلى تغيير شيء ما. وظِّف مديرًا تقنيًا تنفيذيًا (CTO) مستقلًا تُقدّم هذه النصيحة في الغالب للشركات الصغيرة أو الأفراد الذين يستعينون بفريق خارجي لتطوير البرمجيات ويعتمدون على خبراتهم مع الاستمرار في التركيز على تطوير أعمالهم. هذا ليس من الحكمة في شيء؛ يجب أن يكون لديك مدير تقني تنفيذي مستقل (CTO) يقوم بإبلاغك ويتحكم في كيفية عمل الفريق المُتعهِّد. ينبغي التعاقد مع هذا الشخص وفقًا لجدول دفع مختلف مع أهداف وشروط وأهداف مختلفة. في كثير من الأحيان، يحاول أصحاب الأعمال التذاكي في البرمجيات والتحكم في فريق البرنامج مباشرةً، ويتعلمون أساسيات لغة المصطلحات البرمجية ومبادئ DevOps، لكن هذا يقود حتمًا إلى الفشل. كن ذكيا، فأنت تقوم بتطوير الأعمال، سيكون تعاملك المباشر مع مدير التقنية CTO ويتولى هو توجيه الفريق المُتعهِّد ومراقبته؛ بحيث يرسل CTO لك التقارير عن سير العمل، ويرسل له أعضاء الفريق التقارير التقنية بدورهم. حدّد آليةً للمكافآت والعقوبات لا توجد إدارة بدون هذين المكونين الرئيسيين. ليس من المفترض أن تدير جميع المبرمجين في الفريق المُتعهِّد، بل عليك إدارة الفريق بأكمله كوحدة تحكم واحدة. عليك أن تمنحهم نوعًا من التحفيز. ينبغي أن يعرفوا ماذا سينالون إذا نجحوا وكم سيعانون إذا فشلوا. إذا لم توضح هذه الآلية، فستُضطر للتعامل معها ضمنيًا، إذ أن فرص الرّبح تكون منخفضة جدًا. يفترض معظم الناس أن أفضل حافز والحافز الوحيد لفريق البرمجيات هو استمراره في العمل على المشروع. أنت تدفع لهم وهذا يكفي، أليس كذلك؟ هذا خطأ. لا يمكن أن تكون الإدارة فعالة عندما يكون التحويل المصرفي الشهري بمثابة مكافأة وغيابها يعد عقابًا. إنه أسلوب فجّ، وهذا ما يجعله عديم الفعالية إطلاقًا. ابحث عن آلية أفضل وأكثر لباقة وبالتالي أكثر فعالية. لا تقض أكثر من شهر في المشروع يقول بعض الناس 12 أسبوعًا، وأنا أقول شهرًا، ولست وحدي من يقول ذلك. هل تعلم أن الإصدار الأول من Minecraft (الذي بِيعَ لاحقًا إلى Microsoft مقابل 2.5 مليار دولار) تم إصداره في غضون ستة أيام فقط؟ هناك العديد من الأمثلة الأخرى، بما في ذلك Uber و Dropbox و Buffer وغيرها. لقد عبّر إريك ريس في كتابه Lean Startup عن هذا المفهوم بعبارة "منتج الحد الأدنى القابل للتطبيق" (Minimum Viable Product وتختصر إلى MVP)، وأنا متأكد من أنك سمعت هذا الاختصار من قبل. إذا كان المطورون يعدون بتسليم المنتج في غضون بضعة أشهر، فهناك شيء ليس على ما يرام. ينبغي حتمًا أن تحصل على بعض البرامج العاملة في أقل من شهر وينبغي أن تكون جاهزًا للمستخدمين الذين يدفعون رسومًا حقيقية. لقد أنشأت معظم مشاريعي اللطيفة في أقل من أسبوع لكل منها. لا تدفع الرواتب سيريدون منك بطبيعة الحال أن تدفع شهريًا، بالإضافة إلى التأمين الصحي، وأجهزة الحاسوب، والإجازات، ومكتب مشمس لطيف. وهذا هو ما يمكن أن تضيع فيه مخططاتك، إذ أنك تجعلهم سعداء بينما تخسر أنت. عليك إيجاد طريقة لملاءمة أهدافك (تسليم MVP في أقرب وقت ممكن والبدء في جني الأموال) مع أهدافهم (شراء سيارة جديدة وقضاء الإجازة التالية على الشاطئ). هل تستطيع فعل ذلك؟ إنه أمر صعب. لهذا السبب أنشأنا Zerocracy، مما يجعل الفواتير التدريجية والدفع بالنتائج أمرًا ممكنًا. يمكنك إما نقل مشروعك إلى هذه المنصة وإدارة مطوريك هناك، أو ابتكار شيء ما بمفردك. ولكن تذكر، ليست هناك رواتب شهرية! تدفع فقط مقابل النتائج المقدمة. لا تدعهم يجربون يريد كل مبرمج ذكي استخدام مشروعك الجديد كإجراء اختبار لبعض التقنيات الجديدة. لا يحب الناس أن يكرّروا نفس الشيء الذي كانوا يفعلونه بالأمس، إلا إذا كانوا أغبياء ومملين. ولهذا، سيَنصح أعضاء فريقك غالبا باستخدام شيء جديد، قد يكون إطار عمل جديدًا أو قاعدة بيانات جديدة أو حلّ استضافة سحابية جديدًا أو أدوات نشر جديدة. إنهم يفعلون ذلك لأغراضهم الخاصة، وليس لمساعدة المشروع. لا تسقط في هذا الفخ وكن مصرًّا على استخدام ما لديهم من خبرة، على الأقل في مرحلة MVP. يمكنك أن تعدهم بحرية التجربة، ولكن في وقت لاحق عندما يتم إطلاق النسخة MVP. ترجمة -وبتصرف- للمقال Software Outsourcing Survival Guide لصاحبه Yegor Bugayenko
  13. يعدُّ Yeoman (وتلفظ «يومِن» ومعناها في اللغة الأجنبية الخادم الجليل الذي يخدم النبلاء أو الشخص الذي يقدم خدمات جليلة ذات شأن) نظامًا عامًّا للسقالات يسمح بإنشاء أي نوع من التطبيقات، فهو يتيح البدء السريع في مشاريع جديدة ويبسّط صيانة المشاريع القائمة. السقالة باللغة العربية هي هيكل مؤقت يُستخدَم في حمل الأشخاص والمواد بغرض البناء؛ أمَّا في البرمجة، فينطبق التعريف تمامًا عليها أي هي منصةٌ مؤقتةٌ تُستعمَل في بناء هياكل التطبيقات للانطلاق بعملية الإنشاء بسرعة وذلك بتوليد الملفات البدائية الأساسية للمشروع أو التطبيق (الهيكل العام) الذي تود إنشاءه وفق مواصفات ومعايير تحدِّدها مما يوفر عليك الكثير من الوقت. ويمكن القول أنَّ Yeoman محايدٌ مع لغات البرمجة أي يسمح بإنشاء مشاريع بأي لغة تريد (لغات الويب أو لغة جافا أو بايثون أو سي شارب أو غيرها). لا يتخذ Yeoman في حد ذاته أي قرارات. تُتخَذ كل القرارات من قبل المولّدات التي هي في الأساس ملحقات في بيئة Yeoman. هناك الكثير من المولدات المتاحة كما أنّه من السهل إنشاء مولّد جديد يناسب أي سير عمل تريد. Yeoman هو دائمًا خيارك الصحيح لاحتياجات بناء التطبيقات وتوليد ملفات المشروع الابتدائية. فيما يلي بعض حالات الاستخدام الشائعة له: إنشاء مشروع جديد بسرعة إنشاء أقسام جديدة من المشروع، مثل وحدة تحكم جديدة مع اختبارات الوحدة إنشاء وحدات (modules) أو حزم (packages) التمهيد لخدمات جديدة فرض المعايير والممارسات الجيّدة والدلائل النمطية الترويج لمشاريع جديدة من خلال السماح للمستخدمين بالبدء مع تطبيق أو مشروع نموذجي وغيرها من حالات الاستخدام جدول المحتويات حرصًا على تنظيم المقالة ولتسهيل الوصول إلى القسم الذي تريده بسهولة، سنذكر هنا جدول المحتويات باختصار: الجزء الأول: البدء مع Yeoman تثبيت yo وبعض المولدات تنصيب السقالة واستخدامها عمليًا أوامر yo الأخرى استكشاف الأخطاء وإصلاحها الجزء الثاني: تنصيب سقالة Yeoman لبناء تطبيق ويب بناء تطبيق نموذجي باستخدام Yeoman ماذا لدينا في هذه الورشة؟ الخطوة 1: إعداد بيئة التطوير الخاصة بك الخطوة 2: تثبيت مولّد Yeoman الخطوة 3: استخدام مولّد لتنصيب سقالة حول التطبيق وبناء هيكله الخطوة 4: مراجعة بنية التطبيق الذي أنشأته بواسطة Yeoman الخطوة 5: معاينة تطبيقك في المتصفح الخطوة 6: اختبار التطبيق بواسطة Karma و Jasmine الخطوة 7: الاستفادة من التخزين المحلي لحفظ المهام (todos) الخطوة 8: الاستعداد لمرحلة الإنتاج ختام الورشة، تهانينا! المصادر الجزء الأول: البدء مع Yeoman يُعدُّ yo أداة سطر الأوامر في Yeoman التي تسمح بإنشاء مشاريع باستخدام قوالب السقالات (scaffolding templates، يشار إليها باسم «المولدات» [generators]). يُثبَّت yo والمولدات المستخدمة باستعمال مدير حزم نود (npm). تثبيت yo وبعض المولدات تأكد بدايةً من تثبيت Node.js ومدير حزمه npm على حاسوبك. نفِّذ الأمر التالي للتأكد من وجود Node.js و npm لديك: $ nodejs -v v8.10.0 $ npm -v 3.5.2 ننصحك بالرجوع إلى مقال تثبيت Node.js على نظام أبونتو لمزيد من المعلومات حول عملية التثبيت على نظام لينكس. أمَّا على نظام ويندوز، نقترح استخدام أداة سطر أوامر أفضل مثل cmder أو PowerShell لتجربة أفضل. يمكنك دومًا الرجوع إلى موقع Node.js الرسمي واختيار نوع نظام التشغيل لتثبيت Node.js و npm عليه. ثبِّت بعد ذلك yo باستخدام npm عبر الأمر التالي: npm install -g yo بعد ذلك، تُثبَّت المولد(ات) اللازم(ة)، والتي تكون عبارة عن حزم npm تسمى بالشكل generator-XYZ، إذ XYZ هو اسم المولِّد. ابحث عنها على في هذه الصفحة أو عن طريق تحديد خيار قائمة "تثبيت مولد" (install a generator) أثناء في سطر الأوامر yo. إن أردت تثبيت المولد webapp مثلًا، نفِّذ الأمر التالي: npm install -g generator-webapp قد يواجه مستخدمو Node و npm الجدد مشكلات متعلقة بالأذونات. تظهر هذه المشكلات في شكل أخطاء EACCESS أثناء التثبيت. ارجع إلى دليل npm لإصلاح الأذونات إذا حدث لك هذا. تنصيب السقالة واستخدامها عمليًا سنستخدم المولِّد generator-webapp الذي ثبَّتناه للتو في أمثلتنا أدناه. استبدل بالاسم webapp (الذي هو XYZ) اسم المولد الخاص بك للحصول على نفس النتيجة. من أجل تنصيب السقالة في مشروعك الجديد، نفِّذ الأمر التالي: yo webapp ستطرح أغلب المولّدات مجموعة من الأسئلة من أجل تخصيص المشروع الجديد؛ هل تذكر المواصفات والمعايير التي أخبرتك آنفًا بها لتخصيص بها مشروعك؟ ها هي عمليًا. لمعرفة الخيارات المتاحة، استخدم أمر المساعدة : yo webapp --help تعتمد الكثير من المولدات على أنظمة البناء (تعرف بأنها «أدوات بناء» أيضًا) مثل Gulp أو Grunt، ومديري الحزم مثل npm و Bower. تأكد من زيارة موقع المولّد لتتعلّم كيفية تشغيل التطبيق الجديد وصيانته. يمكنك الوصول بسهولة إلى الصفحة الرئيسية للمولّد بتنفيذ الأمر التالي: npm home generator-webapp من المرجّح أن توفر إطارات العمل المعقدة للسقالات مولدات إضافية لبناء الأجزاء الصغيرة من المشروع. يشار إلى هذه المولدات عادةً بالمولدات الفرعية (sub-generators)، ويمكن الوصول إليها كما يلي: generator:sub-generator. لنأخذ المولِّد generator-angular مثالًا. يمكن بمجرد إنشاء تطبيق angular بالكامل إضافة ميزات أخرى. لإضافة وحدة تحكم جديدة إلى المشروع، شغِّل المولد الفرعي لوحدة التحكم: yo angular:controller MyNewController أي أن عمل السقالة لا تقتصر على بناء التطبيق توليد ملفاته الأولية، بل إضافة مزايا وعناصر إليه. أوامر yo الأخرى بخلاف الأساسيات التي غطّيناها في القسم السابق، يعد yo أيضًا أداة تفاعلية بالكامل. ستوفر كتابة yo ببساطة في الطّرفية قائمة من الخيارات لإدارة كل ما يتعلق بالمولدات: التشغيل والتحديث والتثبيت والمساعدة وغيرها من الأدوات. تُوفّرyo أيضا الأوامر التالية: yo --help: تمكنك من الوصول إلى شاشة المساعدة الكاملة yo --generators: تعطي قائمة بكل المولدات المثبتة yo --version: للحصول على الإصدار استكشاف الأخطاء وإصلاحها يمكن إيجاد معظم المشكلات بتنفيذ الأمر: yo doctor سيشخِّص الأمر doctor خطوات لحلّ المشكلات الأكثر شيوعًا. الجزء الثاني: تنصيب سقالة Yeoman لبناء تطبيق ويب أصبحت الآن جاهزًا لبناء تطبيق ويب كامل الوظائف من البداية بالاستعانة بمنصة Yeoman و FountainJS. سيُكتَب نموذج التطبيق في React أو Angular1 أو Angular2. ليس لديك أي فكرة عن React أو Angular؟ لا بأس، سوف نرشدك. ومع ذلك، فإننا نفترض أن لديك تجربة سابقة مع لغة جافاسكريبت. بناء تطبيق نموذجي باستخدام Yeoman سيكون نموذج تطبيق الويب الذي ستنشئه تنفيذًا لمشروع إطار عمل يسمى TodoMVC. ستتمكن من إضافة عناصر مهام لإنجازها (todos) أوحذفها، أو ترشيحها، وسنضيف معًا ميزة لحفظها في وضع عدم الاتصال. ماذا لدينا في هذه الورشة؟ سنبني تطبيق TodoMVC أعلاه من الصفر. كل خطوة سوف تُبنَى على التي تسبقها، لذلك سننتقل من خلال كل خطوة تلو الأخرى: الخطوة 1: إعداد بيئة التطوير الخاصة بك الخطوة 2: تثبيت مولّد Yeoman الخطوة 3: استخدام مولّد لتنصيب سقالة حول التطبيق وبناء هيكله الخطوة 4: مراجعة بنية التطبيق الذي أنشأته بواسطة Yeoman الخطوة 5: معاينة تطبيقك في المتصفح الخطوة 6: اختبار التطبيق بواسطة Karma و Jasmine الخطوة 7: الاستفادة من التخزين المحلي لحفظ المهام (todos) الخطوة 8: الاستعداد لمرحلة الإنتاج سوف تستغرق هذه الخطوات حوالي 25 دقيقة لإتمامها. في الختام، سيكون لديك تطبيق TodoMVC أنيق وسوف يكون جهاز حاسوبك معدًّا لإنشاء المزيد من تطبيقات الويب الرائعة في المستقبل. جاهز؟ لنبدأ مع الخطوة الأولى! الخطوة 1: إعداد بيئة التطوير الخاصة بك ستكون معظم تفاعلاتك مع Yeoman عبر سطر الأوامر. نفّذ الأوامر في الطّرفية إذا كنت تستخدم نظام التشغيل Mac أو في الصدفة shell في نظام لينكس أو في واجهة cmder (وهي المفضلة) أو PowerShell أو cmd.exe إذا كنت تستخدم نظام التشغيل ويندوز. تثبيت المتطلبات الأساسية قبل تثبيت Fountain Webapp Generator، ستحتاج إلى ما يلي: الإصدار السادس من Node.js أو ما بعده. الإصدار الثالث من npm أو ما بعده (والذي يأتي مرفقًا مع Node) نظام التحكم بالإصدارات Git يمكنك أن تتحقق مما إذا كان لديك Node و npm مثبتين بكتابة ما يلي: node --version && npm --version إذا كنت بحاجة إلى ترقية Node أو تثبيته، فإن أسهل طريقة هي استخدام المثبت في نظامك الأساسي. نزِّل حزمة "msi." بالنسبة لنظام ويندوز أو "pkg." لنظام التشغيل Mac من موقع NodeJS ثم ثبِّتها. وإذا كنت تستخدم نظام لينكس، يمكنك تنزيل أحدث إصدار من صفحة التنزيل في الموقع الرسمي. يكون مدير الحزم npm مدمجًا مع Node، رغم أنّك قد تحتاج إلى تحديثه. وتأتي بعض إصدارات Node مع الإصدارات القديمة بدلاً من npm. يمكنك تحديث npm باستخدام هذا الأمر: npm install --global npm@latest يمكنك التحقق من تثبيت Git من خلال كتابة ما يلي: git --version إذا لم يكن لديك Git، حمّل أداة التثبيت من الموقع الرسمي. إن كان نظام التشغيل لديك هو أحد توزيعات ديبيان من لينكس، فنفِّذ الأمر التالي لتثبيت Git: sudo apt install git تثبيت أدوات Yeoman بمجرد تثبيت Node، ثَبِّت أيضًا مجموعة أدوات Yeoman عبر تنفيذ الأمر التالي: npm install --global yo هل هناك أخطاء؟ إذا واجهت أي أخطاء في الأذونات أو الوصول، مثل خطأ EPERM أو EACCESS، فلا تستخدم sudo كحلّ بديل. تستطيع الاستفادة من هذا الدليل. ويمكنك أيضًا الإطلاع على هذا المقال لمعلومات أوسع حول عملية التثبيت. التحقق من نجاح عملية التثبيت من المستحسن التحقق من تثبيت كل شيء كما هو متوقع عبر تنفيذ أوامر Yeoman شائعة الاستخدام مثل yo مع الراية version-- كالتالي: yo --version إصدارات أدوات CLI التي تعمل معها هذه الورشة البرمجية تتغير التكنولوجيا بسرعة! لقد نُفِّذت هذه الورشة التعليمية باستخدام الإصدار 1.8.4 من سطر الأوامر yo. إذا كنت تواجه مشكلات في إصدار أحدث، فسيسعدنا أن نعرف عنها. يمكنك فتح تذكرة في GitHub للإبلاغ بأي مشكلة. الخطوة 1: تثبيت مولد Yeoman ستحتاج في سير عمل تطوير الويب التقليدي إلى قضاء الكثير من الوقت في إعداد الشيفرة المتداولة (boilerplate code) الخاص بتطبيق الويب الذي تعمل عليه، وتنزيل التبعيات، وإنشاء بنية مجلد الويب الخاصة بك يدويًا. ولكن مولّدات Yeoman ستسهّل عليك المهمّة كثيرًا! لنثبِّت مولّدًا لمشاريع FountainJS. تثبيت المولّد يمكنك تثبيت مولدات Yeoman باستخدام الأمر npm وهناك أكثر من 3500 مولد متاح الآن، وكثير منها كتبه أفراد مجتمع المصادر المفتوحة. ثبِّت المُولِّد generator-fountain-webapp باستخدام هذا الأمر: npm install --global generator-fountain-webapp سيبدأ هذا بتثبيت حزم Node المطلوبة للمولد. تذكير: إذا واجهت أي أخطاء في الأذونات أو الوصول، مثل خطأ EPERM أو EACCESS، فلا تستخدم sudo كحلّ بديل. تستطيع الاستفادة من هذا الدليل. في الوقت الذي تستخدم فيه npm install مباشرة، يمكنك البحث عن المولدات عبر قائمة Yeoman التفاعلية. نفِّذ الأمر yo وحدِّد Install a generator للبحث عن المولدات المنشورة. الخطوة 3: استخدام مولّد لتنصيب سقالة حول التطبيق وبناء هيكله لقد استخدمنا كلمة «سقالة» مرات عديدة وشرحنا معناها في بداية هذا الدليل لكن قد ما تزال غريبة بعض الشيء. تعني السقالة، بالمفهوم المستخدم في Yeoman للكلمة (وفي عالم البرمجة عمومًا)، إنشاء ملفات لتطبيق الويب الخاص بك وفق معايير ومواصفات تضبطها أنت. سترى في هذه الخطوة كيف يمكن لـ Yeoman إنشاء ملفات خاصة بمكتبتك أو إطارك المفضل، مع خيارات لاستخدام مكتبات خارجية أخرى مثل Webpack و Babel و SASS وربطها بمشروعك، بأقل جهد ممكن. إنشاء مجلد المشروع أنشئ مجلدًا باسم mytodo ليحوي جميع ملفات المشروع الذي سنعمل على بنائه: mkdir mytodo && cd mytodo هذا المجلد هو المكان الذي سيولِّد المولد فيه ملفات مشروعك الابتدائية (أي هيكل المشروع). الوصول للمولدات عبر قائمة Yeoman نفِّذ الأمر yo لرؤية المولدات الخاصة بك: yo إذا كان لديك عدد قليل من المولدات المثبتة، فستتمكن من الاختيار بشكل تفاعلي منها. اختر المُولِّد Fountain Webapp لتشغيله. استخدام المولدات مباشرة عندما تستأنس أكثر بسطر الأوامر yo، ستفضّل تشغيل المولِّد مباشرةً دون استخدام القائمة التفاعلية (وضغط المزيد من الأزرار) على النحو التالي: yo fountain-webapp إعداد المولد الخاص بك توفر بعض المولدات إعدادات اختيارية لتخصيص تطبيقك مع مكتبات مطوّري البرامج المشتركة لتسريع الإعداد الأولي لبيئة التطوير الخاصة بك. يوفر المولد FountainJS بعض الخيارات لكي تستخدم ما تفضله: إطار العمل (React أو Angular2 أو Angular1) وحدة الإدارة (Webpack أو [SystemJS] أو لا شيء إذا كنت تستعمل Bower) معالج أولي لشيفرة جافاسكربت (Babel أو TypeScript أو لا شيء) معالج أولي لشيفرة CSS ‏(SASS أو LESS أو لا شيء) ثلاثة نماذج للتطبيق (صفحة هبوط وصفحة "مرحبا بالعالم" [hello world] ونموذج TodoMVC) سوف نستخدم لهذه الورشة الخيارات التالية: React Webpack Babel SASS نموذج Redux TodoMVC حدّد هذه الخيارات على التوالي باستخدام مفاتيح الأسهم ثم زر "إدخال" واستمتع بما يحدث على الشاشة. سيبني Yeoman تلقائيًا هيكل تطبيقك ويولد لك جميع الملفات الابتدائية الأساسية له، وسيجلب كل الاعتماديات (dependencies) الخاصة به. بعد بضع دقائق، سنكون مستعدين للانتقال إلى الخطوة التالية. الخطوة 4: مراجعة التطبيق الذي بناه Yeoman افتح المجلّد mytodo الخاص بك لإلقاء نظرة على ما جرى بناؤه إلى الآن. يجب أن يبدو مثلما توضحه الصورة التالية: لدينا في مجلد المشروع المجلدات الفرعية التالية: src: مجلّد رئيسي لتطبيق الويب app: شيفرة React و Redux index.html: ملف html الرئيسي index.js: نقطة الدخول لتطبيق TodoMVC conf : مجلّد رئيسي لملفات الضبط الخاصة بالأدوات الخارجية (Browsersync و Webpack و Gulp و Karma) gulp_tasks و gulpfile.js: مهام أداة البناء Gulp babelrc. و package.json و node_modules: الضبط والاعتماديات المطلوبة gitattributes. و gitignore.: ملفات ضبط Git أنشِئ الإيداع (commit) الأول في المستودع الآن وبعد التوليد والتثبيت، يجب أن يكون لديك مستودع git جديد مهيأ بالفعل. يمكنك الإيداع فيه بأمان لحفظ الحالة الراهنة من خلال هذه الأوامر: git add --all && git commit -m 'First commit' الخطوة 5: استعرض تطبيقك في متصفّح الويب إن أردت معاينة تطبيق الويب الخاص بك في متصفحك المفضل، ليس عليك القيام بأي شيء خاص لإعداد خادم ويب محلي على جهاز حاسوبك، فهو جزء من مهام Yeoman. تشغيل الخادم شغّل سكربت npm لإنشاء خادم http محلي يستند إلى Node على localhost:3000 (أو 127.0.0.1:3000 بالنسبة لبعض التكوينات) بتنفيذ الأمر التالي: npm run serve افتح لسانًا جديدًا في متصفحك وانتقل إلى العنوان 3000:localhost: إيقاف الخادم إذا احتجت في أي وقت إلى إيقاف الخادم، فاستخدم الاختصار Ctrl + C لإنهاء عملية سطر الأوامر الحالية. ملاحظة: لا يمكنك تشغيل أكثر من خادم http واحد على نفس المنفذ (3000 افتراضيًا). مراقبة ملفاتك افتح محرر النصوص الذي تفضّله وابدأ في إجراء التغييرات. سيفرض كل حفظِ لملفاتك تحديث المتصفح تلقائيًا إذ لا يتعين عليك القيام بذلك بنفسك. يُطلق على هذا إعادة التحميل المباشر وهي طريقة رائعة لتعاين حالة تطبيقك في الوقت الفعلي وعكس التغييرات مباشرةً على المتصفح دون أن تفعل أي شيء. تُوَّفر لتطبيقك عملية إعادة التحميل المباشر (Live reloading) من خلال مجموعة من مهام Gulp التي يتم إعدادها في الملف gulpfile.js و Browsersync التي يتم إعدادها في الملف gulp_tasks/browsersync.js؛ فهي تراقب التغييرات التي تطرأ على ملفاتك وتعيد تحميلها تلقائيًا في المتصفح في حالة اكتشاف تغيير ما. فيما يلي أدناه، حرّرنا الملف Header.js القابع في المجلّد src/app/components. بفضل التحديث المباشر، تظهر التغييرات مباشرةً في المتصفح وهذه صورة قبل إجراء أي تعديل: وصورة بعد إجراء تعديل (تغيير عنوان التطبيق) وحفظ الملف: لا تنس الاختبار! لديك تطبيق TodoMVC مجرّب وأنت تقوم بتغيير العنوان. يتعيّن عليك تعديل الاختبار في mytodo/src/app/components/Header.spec.js أو التراجع عن التغيير للتثبت من عملية إعادة التحميل المباشر. الخطوة 6: الاختبار باستخدام Karma و Jasmine إذا لم يكن Karma مألوفًا لديك، فهو برنامج اختبار شيفرات جافاسكربت أي إطار عمل محايد للاختبار. أمّا إطار الاختبار Jasmine فهو مدمج في المولد fountainjs. عندما نفّذنا الأمر yo fountain-webapp في وقت سابق من هذا الدليل، قام المولد بتوليد الملفات ذات النمط spec.js.* في المجلد المصدري لمجلد المشروع mytodo، وأنشأ الملف conf/karma.conf.js، ثم سحب وحدات Node إلى Karma. سنحرّر سكربت Jasmine لوصف الاختبارات قريبًا ولكن دعنا نرى كيف يمكننا إجراء الاختبارات أولًا. تشغيل اختبارات الوحدة دعنا نعود إلى سطر الأوامر ونوقف الخادم المحلي قسرًا باستخدام الاختصار Ctrl + C. يوجد بالفعل سكربت npm أُدرِج في الملف package.json الخاص بنا لإجراء الاختبارات. يمكن تشغيله على النحو التالي: npm test ينبغي أن تنجح جميع الاختبارات. تحديث اختبارات الوحدة ستجد اختبارات الوحدة مهيّأة في المجلد src، لذلك افتح الملف src/app/reducers/todos.spec.js. هذا هو اختبار الوحدة للمخفض (reducer) الخاص بـ Todos. على سبيل المثال، نركز على الاختبار الأول الذي يتحقق من الحالة الابتدائية. (ملاحظة: على ويندوز، قد تحتاج إلى إضافة 127.0.0.1 localhost إلى الملف etc/hosts): it('should handle initial state', () => { expect(todos(undefined, {})).toEqual([ { text: 'Use Redux', completed: false, id: 0 } ]); }); واستبدل هذا الاختبار بما يلي: it('should handle initial state', () => { expect(todos(undefined, {})).toEqual([ { text: 'Use Yeoman', // <=== هنا completed: false, id: 0 } ]); }); عندما نعيد تنفيذ الاختبارات عبر الأمر npm test، ينبغي أن نرى أن الاختبارات تفشل في هذه الحالة. ملاحظة: إذا كنت تريد تشغيل الاختبارات تلقائيًا بعد إجراء أي تعديل، يمكنك استخدام npm run test:auto بدلًا من ذلك. افتح src/app/reducers/todos.js ثم ضع الشيفرة التالية مكان الحالة الابتدائية (initial state): const initialState = [ { text: 'Use Yeoman', completed: false, id: 0 } ]; رائع! لقد أصلحت الاختبار: تسهِّل كتابة اختبارات الوحدة (unit tests) تحديد الأخطاء كلّما أصبح تطبيقك أكبر فأكبر وكلّما انضمّ المزيد من المطورين إلى فريقك. تجعل ميزة الاعتماد على السقالة (scaffolding) في Yeoman كتابة اختبارات الوحدة أسهل، لذا لن يكون لك عذر إذا لم تكتب اختباراتك بيديك! ;-) الخطوة 7: الاستفادة من التخزين المحلي لحفظ المهام (todos) دعنا نعيد النظر في مشكلة العناصر التي لا تثبت (تعاد إلى حالتها الأولية) عندما يُحدَّث المتصفح من خلال تطبيق mytodo مع React/Redux. تنويه: إذا لم يكن الثبات والتخزين المحلي مشكلةً بالنسبة لك أو كان وقتك ضيقًا، فيمكنك تخطي هذه الخطوة والانتقال مباشرة إلى الخطوة 8، "الاستعداد للإنتاج". تثبيت حزمة npm يمكننا استخدام وحدة Redux أخرى تسمى "redux-localstorage" تتيح لنا تنفيذ التخزين المحلي بسرعة وسهولة. نفِّذ الأمر التالي لتثبيتها: npm install --save redux-localstorage@rc استخدام redux-localstorage يجب إعداد مخزن Redux وضبطه لاستخدام التخزين. افتح الملف src/app/store/configStore.js واحذف كل محتواه وضع الشيفرة التالي مكانه: import {compose, createStore} from 'redux'; import rootReducer from '../reducers'; import persistState, {mergePersistedState} from 'redux-localstorage'; import adapter from 'redux-localstorage/lib/adapters/localStorage'; export default function configureStore(initialState) { const reducer = compose( mergePersistedState() )(rootReducer, initialState); const storage = adapter(window.localStorage); const createPersistentStore = compose( persistState(storage, 'state') )(createStore); const store = createPersistentStore(reducer); if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('../reducers', () => { const nextReducer = require('../reducers').default; store.replaceReducer(nextReducer); }); } return store; } إذا عاينت تطبيقك في المتصفح، سترى أن هناك عنصرًا واحدًا "Use Yeoman" في قائمة مهام todo. يعمل التطبيق على تهيئة مخزن مهام todos إذا كانت مساحة التخزين المحلية فارغة ولم نضف فيها أي عناصر حتى الآن. جرب إضافة بعض العناصر إلى القائمة: الآن عندما تُحدِّث المتصفح، نلاحظ أن العناصر موجودة وثابتة. أمرٌ جيدٌ! يمكننا التأكّد من ثبات بياناتنا في التخزين المحلي عبر التحقق من لوحة الموارد (Resources) في أدوات المطور Chrome DevTools واختيار Local Storage في الجانب الأيسر: اكتب اختبارات الوحدة تريد تحديًا إضافيًا؟ حسنًا لك ذلك؛ أعد زيارة اختبارات الوحدة في الخطوة 6 وفكّر في كيفية تحديث اختباراتك الآن إذ تستخدم الشيفرة التخزين المحلي. الخطوة 8: الاستعداد لمرحلة الإنتاج هل أنت مستعد لعرض تطبيقك الجميل؟ دعنا نحاول بناء نسخة جاهزة للإنتاج نستطيع تسليمها للعميل مثلًا. تحسين الملفات من أجل الإنتاج لإنشاء نسخة نهائية من تطبيقنا، نحتاج إلى: فحص الشيفرة بحثا عن أي أخطاء محتملة عبر الأداة lint. تجميع واختصار السكريبتات والأنماط الخاصة بنا لحفظها. تفسير أي مُخرَجات للمعالجات الأولية (preprocessors) التي نستخدمها. وإجمالًا جعل تطبيقنا رشيقًا. ما يثير الدهشة هو أننا نستطيع فعل كل ما سبق فقط بتنفيذ الأمر البسيط التالي: npm run build إنّ تطبيقك الرشيق الجاهز للإنتاج متوفرٌ الآن في المجلد dist في جذر مجلد المشروع mytodo الخاص بك. هذه هي الملفات التي يمكنك وضعها على خادمك باستخدام FTP أو أي خدمة نشر أخرى. بناء ومعاينة التطبيق الجاهز للإنتاج هل تريد معاينة تطبيقك الجاهز للإنتاج محليًا؟ إليك مجرد سكربت npm بسيط آخر: npm run serve:dist سيعمل على بناء مشروعك وإطلاق خادم ويب محلي. إنّه رائع حقًّا! ختام الورشة، تهانينا! كما ترى، يمكن أن يتيح لك Yeoman الكثير والمزيد من الأدوات الخيارات، فيدعم تنصيب السقالات بناء تطبيقات Angular وبعض الإطارات الأخرى التي لم نتطرق لها هنا. على سبيل المثال، يدعم مولد Fountain Angular كذلك إنشاء أنابيب (pipes) وتوجيهات (directives) وخدمات (services) ومكونات (components) جديدة لك. يمكن توليد مكونات جديدة ببساطة وسهولة عبر تنفيذ الأمر: yo fountain-angular2:component componentName، والذي سيؤدي إلى إنشاء ملف المكون الخاص بك وإضافة شيفرة componentName.spec.js جديدة لاختبار الوحدة أيضًا. العثور على المزيد من المولدات الفرعية يمكنك في أي وقت استخدام الأمر: yo --generators للاطلاع على جميع المولدات الفرعية لمولدات Yeoman المثبَّتة. وماذا بعد ذلك؟ Yeoman دائم التطور. احرص على زيارة الموقع yeoman.io للحصول على مزيد من المعلومات ومتابعة Yoman@ على تويتر لمواكبة المستجدات. ساعدتنا مولدات Fountain على كتابة هذا التطبيق Todo بسرعة وأناقة. تابع YeomanFountain@ للبقاء على اطلاع على الميزات الجديدة والإصدارات الجديدة. React هو مكتبة جافا سكريبت لبناء واجهات المستخدم. اطلع على توثيق React في موسوعة حسوب للتعرف على هذه المكتبة أكثر. Angular2 إطار عمل للتطوير على كافة المنصّات. Webpack عبارة عن وحدة مجمعة (module bundler) نموذجية تأخذ الوحدات النمطية ذات الاعتماديات وتولد أصول ثابتة (static assets) تمثل تلك الوحدات. *JSPM حزمة إدارة متصفح. حمّل أي تنسيق للوحدة (ES، و AMD، و CommonJS، وglobals) مباشرة من أي سجل مثل npm و GitHub مع إدارة تبعية حديثة الإصدار (flat versioned dependency management). هذا كل ما ينبغي ذكره الآن بخصوص Yeoman. المصادر Getting Started With Yeoman Let's Scaffold A Web App With Yeoman
  14. لقد كان هناك دائمًا جدل مستمر حول ما إذا كنا بحاجة إلى توسيع مفهوم DevOps من أجل جلب الأمان بوضوح. بعد كل شيء، من الواضح أن DevOps كانت دائمًا اختزالًا لمجموعة واسعة من الممارسات الجديدة باستخدام أدوات جديدة (غالبًا ما تكون مفتوحة المصدر) ومبنية على ثقافات أكثر تعاونًا. ولم لا نتجه نحو DevBizOps لتحسين التوافق مع احتياجات السوق؟ أو DevChatOps للتشديد على نظام اتصال أفضل وأسرع؟ ومع ذلك، كما كتب جون ويليس في وقت سابق من هذا العام عند وقوفه على المصطلحات الخاصة بـالDevSecOps: "نأمل أنه في يوم من الأيام سنكون في عالم ليس علينا أن نستخدم فيه كلمة DevSecOps، وسيكون الأمن جزءًا لا يتجزأ من جميع مناقشات تقديم الخدمة. حتى ذلك اليوم، وفي هذه المرحلة، استنتاجي العام هو أنها مجرد ثلاث حروف جديدة (Sec). والأهم من ذلك أن الاسم يميز حقًا مشكلتنا في عالم لا نقوم فيه بعمل رائع في مجال أمن المعلومات." فلماذا لا نقوم بعمل رائع في مجال أمن المعلومات؟ وماذا يعني القيام بعمل رائع في سياق DevSecOps؟ لم نتمكن من القيام بعمل كبير في مجال أمن المعلومات على الرغم من (أو ربما بسبب) الإنتاج الواسع للبرامج المعقدة التي تعالج المشاكل الضيقة. لكننا أيضًا قمنا بعمل جيد بما فيه الكفاية خلال تلك الفترة عندما كان الدفاع ضد التهديدات يركز على تأمين محيط العمل فقط، وكانت اتصالات الشبكة محدودة، وكان معظم المستخدمين موظفين يستخدمون أجهزة توفرها الشركة. لا تصف هذه الظروف بدقةٍ حقيقة معظم المنظمات منذ عدة سنوات حتى الآن. لكن الحقبة الحالية، التي لم تجلب مفهوم DevSecOps فحسب، بل حتى الأنماط الهيكلية للتطبيقات الجديدة، وممارسات التطوير، وعدد متزايد من التهديدات، تحدد وضعًا طبيعيًا جديدًا وقاسيًا يتطلب وتيرة تغيير أسرع. ليس تغيير منظومة الأمان مرتبطا بالDevSecOps منعزلة، ولكن الأمر يتطلب أمنًا معلوماتيًا بمقاربات جديدة. أمعن النظر في هذه المجالات الخمسة. 1. الأتمتة (Automation) إن السمة المميزة لـ DevOps بشكل عام هي الأتمتة بأكبر قدر ممكن. ويتعلق الأمر جزئياً بالسرعة. إذا كنت ستتحرك بسرعة (دون الإضرار بالأشياء)، فستحتاج حتمًا إلى عمليات قابلة للتكرار تُنفذ دون تدخل بشري كبير. في الواقع، تعد الأتمتة مدخلًا مهمًا لـDevOps، حتى في المؤسسات التي لا تزال تعمل في الغالب على التطبيقات القديمة المتجانسة. وتمثل أتمتة العمليات الروتينية المرتبطة بالتكوينات أو الاختبارات باستخدام أدوات سهلة الاستخدام مثل Ansible دفعةً سريعةً لبدء الطريق إلى DevOps. ليست DevSecOps مختلفة عن هذا المفهوم، فالأمن اليوم هو عملية مستمرة وليس نقطة تفتيش منفصلة في دورة حياة التطبيق، أو حتى فحصًا أسبوعيًا أو شهريًا. عندما يُعثر على الثغرات ويُصدر البائع إصلاحاته، يكون من المهم أن تُطبّق بسرعة لكي تنتهي عمليات استغلال هذه الثغرات الأمنية قريبًا. 2. التحرّك نحو اليسار (Shift left) غالبًا ما يُنظر إلى الأمن التقليدي كحارس بوابة في نهاية عملية التطوير. افحص جميع الصناديق وسيمرّ تطبيقك إلى مرحلة الإنتاج وإلّا حاول مرة أخرى. تُعرف فرق الأمن بأنها لا تكثر الكلام. وبالتالي، لم لا نحرك الأمن مبكرًا نحو اليسار (في الرسم النموذجي من اليسار إلى اليمين لمسلسل التطوير)؟ قد يصرّ الأمنيون على الرفض، ولكن عواقب إعادة الصياغة في مرحلة مبكرة من التطوير هي بالتأكيد أقل بكثير مما يكون عليه الحال عندما يكون التطبيق كاملًا وجاهزًا للتسليم. لا أحب مصطلح "التحرّك نحو اليسار" فذلك يعني أن الأمان لا يزال مجرّد حدثٍ لمرة واحدة نُقل لمرحلة مبكّرة. يجب أن يكون الأمان عملية مؤتمتة إلى حد كبير في كل مكان في دورة حياة التطبيق، من سلسلة التوريد إلى عملية التطوير والاختبار على طول الطريق حتى النشر. 3. إدارة التبعيات (Manage dependencies) أحد التغييرات الكبيرة التي نراها في تطوير التطبيقات الحديثة هو أنك غالبًا لا تكتب معظم الكود. ويعدّ استخدام المكتبات والأطر مفتوحة المصدر مثالًا واضحًا على ذلك. لكن يمكنك أيضًا استخدام الخدمات الخارجية من موفري الخدمات السحابية العامة أو مصادر أخرى. في كثير من الحالات، ستغطي هذه الأكواد والخدمات الخارجية على ما تكتبه بنفسك. نتيجة لذلك، تحتاج منظومة DevSecOps إلى تركيز جادٍّ على سلسلة التوريد الخاصة بالبرنامج. هل تحصل على برنامجك من مصادر موثوقة؟ هل هو مُحدّث؟ هل هو مدمج في عمليات الأمان التي تستخدمها لكودك الخاص؟ ما هي السياسات التي تطبقها في أيّ أكوادٍ وأيّ واجهات لبرمجة التطبيقات قد تستخدمها؟ هل يتوفر الدعم التجاري للمكونات التي تستخدمها لشيفرة المنتج الخاص بك؟ لن تكون هناك مجموعة من الإجابات تناسب جميع الحالات. قد تكون مختلفة في حالة "إثبات الجدوى" عنها في حالة الإنتاج على نطاق واسع. ولكن، كما كان الحال في التصنيع لفترة طويلة (هناك العديد من أوجه الشبه بين DevSecOps وكيفية تطور التصنيع)، تبقى سلامة سلسلة التوريد أمرًا بالغ الأهمية. 4. الرؤية (Vision) لقد تحدثت كثيرًا عن الحاجة إلى الأتمتة خلال جميع مراحل دورة حياة التطبيق. هذا يجعلنا نفترض أنه يمكننا رؤية ما يجري في كل مرحلة من تلك المراحل. تتطلب منظومة DevSecOps الفعالة بالضرورة أدوات فعالة حتى يتضح ما يجب على الأتمتة أن تقوم به. وتندرج هذه الأدوات في عدد من الفئات. هناك مقاييس طويلة الأمد وعالية المستوى تساعدنا في معرفة ما إذا كانت عملية DevSecOps الشاملة تعمل بشكل جيد. وهناك أدوات إنذار حساسة تتطلب تدخلًا بشريًا فوريًا (إذا كان نظام المسح الأمني معطّلًا!). وهناك إنذارات، مثل الفحص الفاشل، تتطلب علاجًا أو إصلاحًا. وهناك سجلات للعديد من البارامترات التي نلتقطها من أجل التحليل لاحقًا (ما الذي يتغير بمرور الوقت؟ وما الذي يتسبب في هذا الفشل؟). 5. الخدمات مقابل الوحدات المتراصة رغم أنه يمكن تطبيق ممارسات DevSecOps على العديد من هياكل تصميمات التطبيقات، إلا أنها تكون أكثر فاعلية من خلال المكونات الصغيرة والموضوعة بشكل فضفاض يتيح تحديثها وإعادة استخدامها دون فرض تغييرات في أماكن أخرى من التطبيق. يمكن أن تكون هذه المكونات في أنقى صورها خدمات مصغرة أو وظائف، لكن المبادئ العامة تنطبق حيثما كان لديك خدمات مقترنة بشكل فضفاض تتواصل عبر الشبكة. يقدم هذا النمط بعض التحديات الأمنية الجديدة. يمكن أن تكون التفاعلات بين المكونات معقدة ويمكن أن تكون المساحة المعرّضة للهجوم أكبر لأن هناك الآن المزيد من نقاط الدخول إلى التطبيق عبر الشبكة. من ناحية أخرى، يدلّ هذا النمط من الهياكل على أن الأمن والمراقبة الآليين يتمتعان أيضًا برؤية أكثر دقة لمكونات التطبيق لأنها لم تعد مدفونة بعمق داخل تطبيق متراصٍّ. لا تنغمس كثيرًا في مصطلح DevSecOps، ولكن عليك أن تتذكر أن الأمن يتطور لأن الطريقة التي نكتب بها وننشر التطبيقات تتطور. ترجمة وبتصرف للمقال ‎5 ways DevSecOps changes security لغوردون هاف