اذهب إلى المحتوى

نشر تطبيق Node.js على الويب: خدمة هيروكو (Heroku) مثالًا


ابراهيم الخضور

سنربط في الفقرات التالية تطبيق الواجهة الأمامية الذي أنشأناه في القسم السابق من هذه السلسلة مع تطبيق الواجهة الخلفية.

رأينا في القسم السابق، أن الواجهة الأمامية قادرة على الوصول إلى قائمة بكل الملاحظات من خادم JSON الذي لعب دور الواجهة الخلفية وذلك بطلب العنوان http://localhost:3001/notes. لكن عنوان الموقع قد تغير قليلًا الآن، وستجد الملاحظات على العنوان http://localhost:3001/api/notes. إذًا، سنغيّر الصفة baseUrl في الملف src/services/notes.js كالتالي:

import axios from 'axios'
const baseUrl = 'http://localhost:3001/api/notes'
const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

// ...

export default { getAll, create, update
 }

لم يعمل الطلب GET إلى العنوان http://localhost:3001/api/notes لسبب ما.

get_request_error_001.png

مالذي يحدث؟ يمكننا التحقق بالولوج إلى الواجهة الخلفية عبر المتصفح باستخدام Postman بلا أدنى مشكلة.

سياسة الجذر المشترك ومفهوم CORS

تأتي التسمية CORS من العبارة Cross-Origin Resource Sharing وتعني هذه العبارة "مشاركة الموارد ذات الجذور المختلطة". ووفقًا لموقع Wikipedia:

اقتباس

مشاركة الموارد ذات الجذور المختلطة هي آلية تسمح بطلب مصادر محددة (مثل تعريف خطوط الكتابة) في صفحة ويب معينة من قبل موقع آخر مختلف عن الموقع الذي نشر هذا المورد للمرة الأولى. حيث يمكن لصفحة ويب أن تضيف الصور ذات الأصول المختلطة وأوراق التسيق المتتالي وسكربتات البرمجة وأطر دمج الموارد في الصفحات iframes ومقاطع الفيديو. وقد تُمنع طلبات بعض أنواع المصادر ذات الأصول المختلطة افتراضيًا مثل طلبات AJAX وفقًا لسياسة خصوصية الجذر المشترك.

تظهر هذه المشكلة في حالتنا بالصورة التالية: لا يمكن لشيفرة JavaScript التي نكتبها لتطبيق الواجهة الأمامية أن يعمل افتراضيًا مع خادم حتى يشتركا بالجذر نفسه. حيث يعمل تطبيق الخادم الذي أنجزناه سابقًا على المنفذ 3001، ويعمل تطبيق الواجهة الأمامية على المنفذ 3000. فلا يملكان جذرًا مشتركًا.

وتذكر أن هذه السياسة لاتنطبق على React و Node فقط، بل هي سياسة عالمية لتشغيل تطبيقات الويب.

يمكننا تخطي ذلك في تطبيقاتنا باستعمال الأداة الوسطية cors التي تقدمها Node. ثَبّت cors باستخدام الأمر التالي:

npm install cors --save

أدرج الأداة واستعملها على النحو:

const cors = require('cors')
app.use(cors())

وهكذا سترى أن تطبيق الواجهة الأمامية سيعمل، لكننا لم نضف حتى الآن وظيفة تغيير أهمية الملاحظة إلى الواجهة الخلفية.

يمكنك الاطلاع على معلومات أكثر عن الأداة CORS من خلال Mozilla's page.

تطبيقات للإنترنت

طالما تأكدنا أن التطبيق بشقيه أصبح جاهزًا للعمل، سننقله إلى الإنترنت. سنستعين بالخادم Heroku لتنفيذ ذلك.

إن لم تستخدم Heroku من قبل، ستجد تعليمات الاستخدام في توثيق Heroku أو بالبحث عبر الإنترنت.

أضف الملف Procfile إلى جذر المشروع لتخبر Heroku كيف سيُشغّل التطبيق.

web: npm start

غيّر تعريف المنفذ الذي نستخدمه في تطبيقنا أسفل الملف index.js ليصبح كالتالي:

const PORT = process.env.PORT || 3001
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

وهكذا فإما أن سنستخدم المنفذ الذي عرّفناه كمتغيير بيئة أو المنفذ 3001 إن لم يُعرّف منفذ من خلال متغير البيئة. يُهيِّئ Heroku منفذ التطبيق بناء على قيمة منفذ متغير البيئة.

أنشئ مستودع git في جذر المشروع وأضف الملف ذو اللاحقة gitignore. وفيه المعلومات التالية:

node_modules

أنشئ تطبيق Heroku بتنفيذ الأمر heroku create، حمل شيفرتك إلى المستودع ثم انقله إلى Heroku بتنفيذ الأمر git push heroku master. إن جرى كل شيء على مايرام، سيعمل التطبيق:

backend_heroku_works_002.png

إن لم يعمل التطبيق، تحقق من المشكلة بقراءة سجلات Heroku، وذلك بتنفيذ الأمر heroku logs.

من المفيد في البداية أن تراقب باستمرار ما يظهره Heroku من سجلات. وأفضل وسيلة للمراقبة تنفيذ الأمر heroku log -t الذي يطبع سجلاته على الطرفية، عندما تحصل مشكلة ما على الخادم.

إن كنت ستنشر تطبيقك من مستودع git ولم تكن الشيفرة موجودة في الفرع الرئيسي (إي إن كنت ستعدل مستوع الملاحظات من الدرس السابق)، عليك استخدام الأمر git push heroku HEAD:master. وإن فعلت ذلك مسبقًا، ربما ستحتاج إلى تنفيذ هذا الأمر git push heroku HEAD:master --force.

تتكامل الواجهة الخلفية مع الأمامية على Heroku أيضًا. يمكنك التحقق من ذلك بتغيير عنوان الواجهة الخلفية الموجود ضمن شيفرة الواجهة الأمامية، ليصبح نفس عنوان الواجهة الخلفية على Heroku بدلًا من http://localhost:3001

السؤال التالي هو: كيف سننشر تطبيق الواجهة الأمامية على الإنترنت؟ لدينا العديد من الخيارات.

بناء نسخة الإنتاج من الواجهة الأمامية

لقد أنشأنا حتى هذه اللحظة تطبيقات React في وضعية التطوير (development mode)، حيث يهيأ التطبيق لإعطاء رسائل خطأ واضحة، وتُصيّر الشيفرة مباشرة عند حدوث أية تغييرات وهكذا.

لكن عندما يغدو التطبيق جاهزًا للنشر لابد من إنشاء نسخة إنتاج (production build) أو نسخة التطبيق المتمثلة للإنتاج.

ننشئ نسخة الإنتاج من التطبيقات التي بنيت باستخدام create-react-app بتنفيذ الأمر npm run build. لننفذ الأمر السابق عند جذر مشروع الواجهة الأمامية. ينشئ تنفيذ الأمر السابق مجلدًا يدعى build (يحوي ملف HTML الوحيد للتطبيق ويدعى index.html) وفي داخله مجلد آخر يدعى static ستوَلَّد فيه نسخة مصغرة عن شيفرة JavaScript للتطبيق. وعلى الرغم من وجود عدة ملفات JavaScript في تطبيقنا، إلا أنها ستُجمّع ضمن ملف مصغّر واحد. وفي واقع الأمر، سيَضم هذا الملف كل محتويات ملفات الارتباط الخاصة بالتطبيق أيضًا.

لن يكون فهم أو قراءة محتويات هذا الملف يسيرًا كما ترى:

!function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c<i.length;c++)f=i[c],o[f]&&s.push(o[f][0]),o[f]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var l=t[i];0!==o[l]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={2:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})

تقديم الملفات ذات المحتوى الثابت من الواجهة الخلفية

إن أحد الطرق المتبعة في نشر الواجهة الأمامية هو بناء نسخة إنتاج ووضعها في جذر المستودع الذي يحتوي الواجهة الخلفية. ومن ثم نهيئ الواجهة الخلفية لتعرض الصفحة الرئيسية للواجهة الأمامية (build/index.html).

نبدأ العملية بنقل نسخة الإنتاج إلى جذر الواجهة الخلفية. استخدم الأمر التالي في إجراء عملية النسخ إن كنت تستخدم أحد نظامي التشغيل Mac أو Linux

cp -r build ../../../osa3/notes-backend

واستخدم في النظام windows إحدى التعليميتن copy أو xcopy أو استخدم ببساطة النسخ و اللصق. سيبدو مجلد الواجهة الخلفية كالتالي:

backend_root_003.png

سنستخدم أداة وسطية مدمجة مع المكتبة express تعرض الملفات ذات المحتوى الثابت التي تحضرها من الخادم مثل الملف index.html وملفات JavaScript وغيرها، تدعى هذه الأداة static. فعندما نضيف العبارة التالية إلى شيفرة الواجهة الخلفية:

app.use(express.static('build'))

ستتحقق Express من محتويات المجلد build عندما تتلقى أية طلبات HTTP-GET. فإن وجدت ملفًا مطابقًا للملف المطلوب ستعيده. وهكذا ستظهر الواجهة الأمامية المبنية باستخدام React، عندما يستجيب الخادم إلى طلبات GET إلى العنوان www.serversaddress.com/index.html أو إلى العنوان www.serversaddress.com. بينما تتعامل الواجهة الخلفية مع الطلب GET إلى العنوان www.serversaddress.com/api/notes.

في حالتنا هذه سنجد أن للواجهتين العنوان نفسه، لذلك نستطيع أن نعطي للمتغير baseUrl عنوان موقع نسبي، وذلك كي لانذكر القسم الأول من العنوان والمتعلق بالخادم.

import axios from 'axios'
const baseUrl = '/api/notes'
const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
// ...
}

بعد إجراء تلك التغييرات، علينا أن ننشئ نسخة إنتاج ونضعها في جذر مستودع الواجهة الخلفية. وبالتالي سنصبح قادرين على استخدام الواجهة الأمامية بطلب عنوان الواجهة الخلفية http://localhost:3001.

localhost_call_004.png

وهكذا سيعمل تطبيقنا تمامًا كتطبيق الصفحة الواحدة النموذجي الذي درسناه في القسم 0. فعندما نطلب عنوان الواجهة الخلفية http://localhost:3001 سيعيد الخادم الملف index.html من المجلد build. ستجد محتوى الملف (بشكل مختصر) كالتالي:

<head>
  <meta charset="utf-8"/>
  <title>React App</title>
  <link href="/static/css/main.f9a47af2.chunk.css" rel="stylesheet">
</head>
<body>
  <div id="root"></div>
  <script src="/static/js/1.578f4ea1.chunk.js"></script>
  <script src="/static/js/main.104ca08d.chunk.js"></script>
</body>
</html>

يتضمن الملف تعليمات لإحضار ملف CSS الذي يعرف تنسيقات عناصر التطبيق ومعرّفي شيفرة (script tag) يوجهان المتصفح إلى إحضار شيفرة JavaScript التي يحتاجها التطبيق، وهي الشيفرة الفعلية لتطبيق React.

تحضر شيفرة React الملاحظات من العنوان http://localhost:3001/api/notes، وتصيّرها على الشاشة. يمكنك أن تتابع تفاصيل الاتصال بين المتصفح والخادم من نافذة Network ضمن طرفية التطوير:

browser_server_comm_005.png

بعد أن نتأكد من عمل نسخة الإنتاج على الخادم المحلي، انقل المجلد build الذي يحوي الواجهة الأمامية إلى مستودع الواجهة الخلفية، ثم انقل الشيفرة كلها إلى خادم Heroku مجددًا.

سيعمل التطبيق بشكل جيد، ماعدا الجزئية التي تتعلق بتغيير أهمية الملاحظات.

app_works_on_heoku_006.png

حتى اللحظة يحفظ تطبيقنا الملاحظات ضمن متغيّر، وبالتالي إذا ما حدث أمر ما وتوقف التطبيق عن العمل ستختفي تلك الملاحظات. إذًا لايزال التطبيق بحاجة إلى قاعدة بيانات. سنشرح تاليًا عددًا من النقاط قبل أن ننتقل إلى قاعدة البيانات.

نشر الواجهة الخلفية بطريقة انسيابية

سنكتب سكربت npm بسيط لإنشاء نسخة إنتاج من الواجهة الأمامية بأقل جهد ممكن. ضع الشيفرة التالية في ملف package.json الموجود في مستودع الواجهة الخلفية:

{
  "scripts": {
    //...
    "build:ui": "rm -rf build && cd ../../osa2/materiaali/notes-new && npm run build --prod && cp -r build ../../../osa3/notes-backend/",
    "deploy": "git push heroku master",
    "deploy:full": "npm run build:ui && git add . && git commit -m uibuild && npm run deploy",    
    "logs:prod": "heroku logs --tail"
  }
}

عندما ينفذ npm الجزء (build:ui) من السكريبت السابقة بالأمر npm run build:ui، سيبني نسخة إنتاج من الواجهة الأمامية ضمن مستودع الواجهة الخلفية. بينما سينشر الأمر npm run deploy النسخة الحالية للواجهة الخلفية على الخادم Heroku. وأخيرًا يدمج الأمر npm run deploy:full التطبيقين معًا ويزودهما بأوامر git تساعد في تحديث مستودع الواجهة الخلفية.

يبقى هناك جزء من سكريبت npm يُنفَّذ بالأمر npm run logs:prod، ويُستخدَم لطباعة سجلات Heroku أثناء التنفيذ.

انتبه: تعتمد المسارات المكتوبة في الجزء build:ui من السكربت على موقع المستودعات في منظومة الملفات.

تُنفَّذ npm سكريبت في نظام windows باستخدام cmd.exe لأن واجهة النظام (shell) الافتراضية لاتدعم أوامر أو واجهة bash. ولتنفيذ الأوامر السابقة استبدل واجهة النظام بالواجهة bash باستخدام محرر Git الافتراضي في windows كالتالي:

npm config set script-shell "C:\\Program Files\\git\\bin\\bash.exe"

الخادم الوكيل

لن تعمل الواجهة الأمامية بعد التغييرات التي أجريناها في وضعية التطوير (عندما تُشغَّل باستخدامnpm start)، لأن الاتصال مع الواجهة الخلفية لن يعمل.

dev_mode_change_007.png

وذلك لتغيّر عنوان الواجهة الخلفية إلى عنوان نسبي:

const baseUrl = '/api/notes'

لقد كان عنوان الواجهة الأمامية في مرحلة التطوير localhost:3000، وبالتالي ستُرسل الطلبات الآن إلى الواجهة الخلفية على العنوان الخاطئ localhost:3000/api/notes. وطبعًا الواجهة الخلفية موجودة فعلًا على العنوان localhost:3001.

سيكون الحل بسيطًا عندما ننشئ المشروع باستخدام create-react-app. أضف التصريحات التالية إلى الملف package.json الموجود في مستودع الواجهة الأمامية:

{
  "dependencies": {
    // ...
  },
  "scripts": {
    // ...
  },
  "proxy": "http://localhost:3001"
}

بعد إعادة تشغيل التطبيق، ستعمل بيئة تطوير React كخادم وكيل. فلو أرسلت شيفرة React طلبًا إلى خادم على العنوان http://localhost:3000، ولم يكن تطبيق React هو من يدير هذا الخادم (أي في الحالة التي لا تحضر فيها الطلبات ملفات CSS أو JavaScript الخاصة بالتطبيق)، سيعاد توجيه الطلب إلى الخادم الذي عنوانه http://localhost:3001. وهكذا ستعمل الواجهة الأمامية بشكل جيد في وضعي التطوير والإنتاج.

إن سلبية هذه المقاربة هي التعقيد الذي تظهره في نشر الواجهة الأمامية على الإنترنت. فنشر نسخة جديدة سيتطلب إنشاء نسخة إنتاج جديدة ونقلها إلى مستودع الواجهة الخلفية. وسيصعّب ذلك إنشاء خطوط نشر آلية. وتعرّف خطوط النشر بأنها طريقة مؤتمتة وقابلة للتحكم لنقل الشيفرة من حاسوب المطور إلى بيئة الإنتاج مرورًا باختبارات مختلفة بالإضافة إلى التحقق من الجودة.

يمكن إنجاز ذلك بطرق عدة، منها وضع الواجهتين الخلفية والأمامية في نفس المستودع. لكننا لن نناقش ذلك الآن. في بعض الحالات من المعقول أن ننشر الواجهة الأمامية على شكل تطبيق مستقل وهذا أمر بسيط ويطبق مباشرة إن كتب التطبيق باستخدام create-react-app.

ستجد الشيفرة الحالية للواجهة الخلفية في الفرع part3-3 على Github. بينما ستجد التعديلات على الواجهة الأمامية في الفرع part3-1 في مستوودع الواجهة الأمامية.

التمارين 3.9 - 3.11

لايتطلب إنجاز التمارين التالية العديد من الأسطر البرمجية. لكنها ستحمل شيئًا من التحدي، لأنها تتطلب فهمًا دقيقًا لما يحدث وأين يحدث، بالإضافة إلى تهيئة التطبيق كما يجب.

3.11 دليل هاتف للواجهة الخلفية: الخطوة 9

اجعل الواجهة الخلفية تتكامل مع الأمامية في نفس التمرين من القسم السابق. لا تضف وظيغة تغيير الأرقام حاليًا، بل سنقوم بذلك لاحقًا. ربما ستغيّر قليلًا في الواجهة الأمامية، على الأقل عناوين مواقع الواجهة الخلفية. وتذكر أن تبقي طرفية التطوير مفتوحة في متصفحك. تحقق مما يحدث إن فشل أي طلب HTTP من خلال النافذة Network، وابق نظرك دائما على طرفية تطوير الواجهة الخلفية في نفس الوقت. إن لم تتمكن من إنجاز التمرين السابق، ستفيدك طباعة بيانات الطلب أوجسم الطلب على الطرفية بزرع تعليمة الطباعة في دالة معالج الحدث المسؤولة عن طلبات POST.

3.10 دليل هاتف للواجهة الخلفية: الخطوة 10

انشر الواجهة الخلفية على الإنترنت (على Heroku مثلًا).

ملاحظة: إن لم تستطع لسبب ما تثبيت Heroku على حاسوبك، استخدم الأمر npx heroku-cli.

اختبر الواجهة الخلفية التي نشرتها من خلال المتصفح بمساعدة Postman أو VS Code REST client للتأكد من أنها تعمل.

نصيحة للاحتراف: عندما تنشر تطبيقك على Heroku، يفضل -على الأقل في البداية- أن تبقي نظرك على ما يطبعه تطبيق Heroku دائمًا، وذلك بتنفيذ الأمر heroku logs -t.

تمثل الصورة التالية مايطبعه heroku عندما لا يستطيع إيجاد ملف الارتباط express.

heroku_error_no _express_008.png

والسبب أن الخيار save-- لم يحدد عندما ثُبتت المكتبة express. وبالتالي لم تحفظ معلومات ملف الارتباط ضمن الملف package.json.

أحد الأخطاء الأخرى، هو أن التطبيق لم يهيأ لاستعمال المنفذ الذي عُرِّف في متغير البيئة PORT:

heroku_erro_port_009.png

أنشئ ملفًا باسم README.md في جذر مستودعك، وأضف إليه رابطًا لتطبيقك على الإنترنت.

3.11 دليل هاتف للواجهة الخلفية: الخطوة 11

أنشئ نسخة إنتاج من الواجهة الأمامية، وانشرها على الإنترنت بالأسلوب الذي ناقشناه في هذا الفصل.

ملاحظة: تأكد من أن المجلد build ليس ضمن قائمة الملفات التي يهملها git، وهو الملف الذي لاحقته gitignore. تأكد أيضًا أن الواجهة الأمامية لازالت تعمل.

ترجمة -وبتصرف- للفصل Deploying App to Internet من سلسلة Deep Dive Into Modern Web Development


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...