الجزء 3: البرمجة من جهة الخادم

نشر التطبيق على الإنترنت

لنربط الآن الواجهة الأمامية التي أنشأناها في الجزء 2 بالواجهة الخلفية Express التي بنيناها في الجزء 3أ.

في الجزء 2، كانت الواجهة الأمامية تجلب الملاحظات من json-server على العنوان http://localhost:3001/notes. الواجهة الخلفية Express التي بنيناها في هذا الجزء لها بنية URL مختلفة قليلاً — الملاحظات الآن على العنوان 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 يُظهر خطأً في أدوات التطوير

ما الذي يجري هنا؟ يمكننا الوصول إلى الواجهة الخلفية من المتصفح ومن postman دون أي مشاكل.

سياسة الأصل نفسه وCORS

تكمن المشكلة في شيء يُسمى سياسة الأصل نفسه (same origin policy). يُعرَّف أصل عنوان URL بمزيج من البروتوكول (ويُسمى أيضاً المخطط scheme) واسم المضيف والمنفذ.

http://example.com:80/index.html
  
protocol: http
host: example.com
port: 80

عندما تزور موقعاً إلكترونياً (مثل http://example.com)، يُرسل المتصفح طلباً إلى الخادم الذي يُستضاف عليه الموقع (example.com). الاستجابة التي يرسلها الخادم هي ملف HTML قد يحتوي على مرجع واحد أو أكثر إلى أصول أو موارد خارجية مستضافة إما على الخادم نفسه الذي يُستضاف عليه example.com أو على موقع آخر. عندما يرى المتصفح مرجعاً أو مراجع إلى عنوان URL في HTML المصدر، يُرسل طلباً. إذا أُرسل الطلب باستخدام عنوان URL نفسه الذي جُلب منه HTML المصدر، فسيعالج المتصفح الاستجابة دون أي مشاكل. أما إذا جُلب المورد باستخدام عنوان URL لا يشارك الأصل نفسه (المخطط، المضيف، المنفذ) مع HTML المصدر، فسيتعين على المتصفح فحص ترويسة الاستجابة Access-Control-Allow-origin. وإذا احتوت على * أو على عنوان URL الخاص بـ HTML المصدر، فسيعالج المتصفح الاستجابة، وإلا فسيرفض معالجتها ويرمي خطأً.

إن سياسة الأصل نفسه آلية أمنية ينفّذها المتصفحات لمنع اختطاف الجلسات (session hijacking) وغيرها من الثغرات الأمنية.

لتمكين الطلبات المشروعة عبر الأصول (الطلبات إلى عناوين URL التي لا تشارك الأصل نفسه)، ابتكرت W3C آلية تُسمى CORS (مشاركة الموارد عبر الأصول، Cross-Origin Resource Sharing). وفقاً لـويكيبيديا:

مشاركة الموارد عبر الأصول (CORS) آلية تسمح بطلب موارد مقيّدة (مثل الخطوط) في صفحة ويب من نطاق آخر خارج النطاق الذي قُدّم منه المورد الأول. ويمكن لصفحة الويب أن تضمّن بحرية صوراً وأوراق أنماط وسكربتات وإطارات iframe وفيديوهات من أصول مختلفة. بعض الطلبات «عبر النطاقات»، وبخاصة طلبات Ajax، محظورة افتراضياً بموجب سياسة الأمان للأصل نفسه.

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

ضع في اعتبارك أن سياسة الأصل نفسه وCORS ليستا خاصتين بـ React أو Node. إنهما مبدآن عامان يتعلقان بالتشغيل الآمن لتطبيقات الويب.

يمكننا السماح بالطلبات من أصول أخرى باستخدام الوسيط cors الخاص بـ Node.

في مستودع الواجهة الخلفية لديك، ثبّت cors بالأمر

npm install cors

خُذ الوسيط لاستخدامه واسمح بالطلبات من جميع الأصول:

const cors = require('cors')

app.use(cors())

ملاحظة: عند تمكين cors، ينبغي أن تفكر في كيفية إعداده. في حالة تطبيقنا، وبما أنه لا يُتوقع أن تكون الواجهة الخلفية مرئية للعامة في بيئة الإنتاج، فقد يكون من المنطقي أكثر تمكين cors من أصل محدد فقط (مثل الواجهة الأمامية).

الآن تعمل معظم ميزات الواجهة الأمامية! لم تُنفَّذ بعد في الواجهة الخلفية وظيفة تغيير أهمية الملاحظات، لذا بطبيعة الحال لا تعمل بعد في الواجهة الأمامية. سنصلح ذلك لاحقاً.

يمكنك قراءة المزيد عن CORS من صفحة Mozilla.

يبدو إعداد تطبيقنا الآن كما يلي:

مخطط تطبيق React والمتصفح

يجلب تطبيق React العامل في المتصفح الآن البيانات من خادم node/express العامل على localhost:3001.

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

الآن بعد أن أصبحت المنظومة كاملة جاهزة، لننقل تطبيقنا إلى الإنترنت.

هناك عدد متزايد باستمرار من الخدمات التي يمكن استخدامها لاستضافة تطبيق على الإنترنت. تتولى الخدمات الصديقة للمطورين مثل PaaS (أي المنصة كخدمة، Platform as a Service) أمر تثبيت بيئة التنفيذ (مثل Node.js)، ويمكنها أيضاً توفير خدمات متنوعة مثل قواعد البيانات.

على مدى عقد من الزمن، هيمنت Heroku على مشهد PaaS. لسوء الحظ انتهت الطبقة المجانية من Heroku في 27 نوفمبر 2022. هذا مؤسف جداً لكثير من المطورين، وخاصة الطلاب. لا يزال Heroku خياراً قابلاً للتطبيق جداً إذا كنت مستعداً لإنفاق بعض المال. لديهم أيضاً برنامج للطلاب يوفر بعض الأرصدة المجانية.

نقدّم الآن خدمتين: Fly.io وRender. توفّر Fly.io مرونة أكبر كخدمة، لكنها أصبحت مؤخراً مدفوعة أيضاً. توفّر Render بعض وقت الحوسبة المجاني، لذا إذا أردت إكمال الدورة دون تكاليف، فاختر Render. قد يكون إعداد Render أسهل أيضاً في بعض الحالات، لأن Render لا يتطلب أي عمليات تثبيت على جهازك.

هناك أيضاً بعض خيارات الاستضافة المجانية الأخرى التي تعمل جيداً مع هذه الدورة، على الأقل لجميع الأجزاء عدا الجزء 11 (CI/CD) الذي قد يحتوي على تمرين صعب بالنسبة للمنصات الأخرى.

استخدم بعض المشاركين في الدورة الخدمات التالية أيضاً:

إذا كنت تعرف خدمات مجانية وسهلة الاستخدام لاستضافة NodeJS، فأخبرنا!

بالنسبة لكل من Fly.io وRender، علينا تغيير تعريف المنفذ الذي يستخدمه تطبيقنا في أسفل ملف index.js في الواجهة الخلفية كما يلي:

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

نستخدم الآن المنفذ المعرّف في متغير البيئة PORT أو المنفذ 3001 إذا كان متغير البيئة PORT غير معرّف. من الممكن إعداد منفذ التطبيق بناءً على متغير البيئة في كل من Fly.io وRender.

Fly.io

لاحظ أنك قد تحتاج إلى تقديم رقم بطاقتك الائتمانية إلى Fly.io!

إذا قررت استخدام Fly.io فابدأ بتثبيت برنامج flyctl التنفيذي باتباع هذا الدليل. بعد ذلك، ينبغي أن تنشئ حساب Fly.io.

ابدأ بـالمصادقة عبر سطر الأوامر بالأمر

fly auth login

لاحظ أنه إذا لم يعمل الأمر fly على جهازك، يمكنك تجربة الصيغة الأطول flyctl. مثلاً على MacOS، تعمل كلتا صيغتَي الأمر.

إذا لم تنجح في تشغيل flyctl على جهازك، يمكنك تجربة Render (انظر القسم التالي)، فهو لا يتطلب تثبيت أي شيء على جهازك.

تتم تهيئة تطبيق بتشغيل الأمر التالي في المجلد الجذر للتطبيق

fly launch --no-deploy

أعطِ التطبيق اسماً أو دع Fly.io تولّد واحداً تلقائياً. اختر منطقة سيُشغَّل فيها التطبيق. لا تُنشئ قاعدة بيانات Postgres للتطبيق ولا تُنشئ قاعدة بيانات Upstash Redis، لأنها غير مطلوبة.

يُنشئ Fly.io ملف fly.toml في جذر تطبيقك حيث يمكننا إعداده. لتشغيل التطبيق، قد نحتاج إلى إضافة صغيرة إلى الإعدادات:

[build]

[env]
  PORT = "3001" # أضف هذا

[http_service]
  internal_port = 3001 # تأكد من أن هذا مطابق لـ PORT
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ["app"]

لقد عرّفنا الآن في القسم [env] أن متغير البيئة PORT سيحصل على المنفذ الصحيح (المعرّف في القسم [http_service]) الذي ينبغي أن ينشئ التطبيق الخادم عليه.

أصبحنا الآن جاهزين لنشر التطبيق على خوادم Fly.io. يتم ذلك بالأمر التالي:

fly deploy

إذا سار كل شيء على ما يرام، ينبغي أن يكون التطبيق الآن يعمل. يمكنك فتحه في المتصفح بالأمر

fly apps open

من الأوامر المهمة بشكل خاص fly logs. يمكن استخدام هذا الأمر لعرض سجلات الخادم. من الأفضل إبقاء السجلات ظاهرة دائماً!

ملاحظة: قد تُنشئ Fly آلتين (machines) لتطبيقك، وإذا حدث ذلك فستكون حالة البيانات في تطبيقك غير متسقة بين الطلبات، أي سيكون لديك آلتان لكل منهما متغير notes الخاص بها، فقد ترسل POST إلى آلة ثم يذهب طلب GET التالي إلى آلة أخرى. يمكنك التحقق من عدد الآلات باستخدام الأمر "$ fly scale show"، وإذا كان COUNT أكبر من 1 فيمكنك فرضه ليكون 1 بالأمر "$ fly scale count 1". يمكن أيضاً التحقق من عدد الآلات على لوحة التحكم.

ملاحظة: في بعض الحالات (السبب غير معروف حتى الآن) تسبّب تشغيل أوامر Fly.io، وخاصة على Windows WSL (النظام الفرعي لنظام Windows الخاص بـ Linux)، في مشاكل. إذا تعطّل الأمر التالي دون استجابة

flyctl ping -o personal

فلن يتمكن حاسوبك لسبب ما من الاتصال بـ Fly.io. إذا حدث لك هذا، هذا الرابط يصف طريقة ممكنة للمتابعة.

إذا كان ناتج الأمر أدناه بهذا الشكل:

$ flyctl ping -o personal
35 bytes from fdaa:0:8a3d::3 (gateway), seq=0 time=65.1ms
35 bytes from fdaa:0:8a3d::3 (gateway), seq=1 time=28.5ms
35 bytes from fdaa:0:8a3d::3 (gateway), seq=2 time=29.3ms
...

فلا توجد مشاكل في الاتصال!

كلما أجريت تغييرات على التطبيق، يمكنك نقل الإصدار الجديد إلى الإنتاج بالأمر

fly deploy

Render

لاحظ أنك قد تحتاج إلى تقديم رقم بطاقتك الائتمانية إلى Render!

يفترض ما يلي أن تسجيل الدخول قد تم بحساب GitHub.

بعد تسجيل الدخول، لننشئ "خدمة ويب" جديدة:

صورة تُظهر خيار إنشاء خدمة ويب جديدة

ثم يُربط مستودع التطبيق بـ Render:

صورة تُظهر مستودع التطبيق على Render.

يبدو أن الربط يتطلب أن يكون مستودع التطبيق عاماً.

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

صورة تُظهر حقل المجلد الجذر كخيار اختياري

بعد ذلك، يبدأ التطبيق في Render. تخبرنا لوحة التحكم بحالة التطبيق وعنوان URL الذي يعمل عليه التطبيق:

يُظهر الركن الأيسر العلوي من الصورة حالة التطبيق وعنوان URL الخاص به

وفقاً لـالتوثيق، ينبغي أن يعيد كل commit إلى GitHub نشر التطبيق. لسبب ما لا يعمل هذا دائماً.

لحسن الحظ، من الممكن أيضاً إعادة نشر التطبيق يدوياً:

قائمة مع إبراز خيار نشر أحدث commit

كما يمكن رؤية سجلات التطبيق في لوحة التحكم:

صورة مع إبراز تبويب السجلات في الركن الأيسر. وعلى اليمين سجلات التطبيق

نلاحظ الآن من السجلات أن التطبيق بدأ على المنفذ 10000. تحصل شيفرة التطبيق على المنفذ الصحيح عبر متغير البيئة PORT، لذا من الضروري أن يكون ملف index.js قد حُدِّث في الواجهة الخلفية كما يلي:

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

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

حتى الآن كنا نشغّل شيفرة React في وضع التطوير. في وضع التطوير يُهيَّأ التطبيق لإعطاء رسائل خطأ واضحة، وعرض تغييرات الشيفرة في المتصفح فوراً، وما إلى ذلك.

عند نشر التطبيق، يجب إنشاء بناء للإنتاج أو نسخة من التطبيق مُحسَّنة للإنتاج.

يمكن إنشاء بناء للإنتاج للتطبيقات المنشأة بـ Vite بالأمر npm run build.

لننفّذ هذا الأمر من جذر مشروع الملاحظات في الواجهة الأمامية الذي طوّرناه في الجزء 2.

يُنشئ هذا مجلداً يُسمى dist يحتوي على ملف HTML الوحيد لتطبيقنا (index.html) والمجلد assets. ستُولَّد نسخة مُصغَّرة من شيفرة JavaScript لتطبيقنا في مجلد dist. ورغم أن شيفرة التطبيق موزعة على عدة ملفات، فستُصغَّر كل شيفرة 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"})

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

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

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

cp -r dist ../backend

إذا كنت تستخدم حاسوباً يعمل بـ Windows، فيمكنك استخدام الأمر copy أو xcopy بدلاً من ذلك. وإلا فانسخ والصق ببساطة.

ينبغي أن يبدو مجلد الواجهة الخلفية الآن كما يلي:

لقطة شاشة bash لأمر ls تُظهر مجلد dist

لجعل Express يعرض المحتوى الثابت، أي صفحة index.html وJavaScript وغيرها مما يجلبه، نحتاج إلى وسيط مدمج في Express يُسمى static.

عندما نضيف ما يلي بين تعريفات الوسطاء

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

فكلما تلقى Express طلب HTTP GET سيتحقق أولاً مما إذا كان مجلد dist يحتوي على ملف يقابل عنوان الطلب. وإذا وُجد ملف مطابق، فسيعيده Express.

الآن ستعرض طلبات HTTP GET إلى العنوان www.serversaddress.com/index.html أو www.serversaddress.com الواجهة الأمامية React. أما طلبات GET إلى العنوان www.serversaddress.com/api/notes فستتولاها شيفرة الواجهة الخلفية.

نظراً لحالتنا، حيث تقع الواجهة الأمامية والواجهة الخلفية على العنوان نفسه، يمكننا تعريف baseUrl كعنوان URL نسبي. هذا يعني أنه يمكننا حذف الجزء الذي يعرّف الخادم.

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:3001

يعمل تطبيقنا الآن تماماً مثل التطبيق المثالي أحادي الصفحة الذي درسناها في الجزء 0.

عندما نستخدم المتصفح للانتقال إلى العنوان http://localhost:3001، يعيد الخادم ملف index.html من مجلد dist. محتوى الملف كما يلي:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
    <script type="module" crossorigin src="/assets/index-5f6faa37.js"></script>
    <link rel="stylesheet" href="/assets/index-198af077.css">
  </head>
  <body>
    <div id="root"></div>
    
  </body>
</html>


يحتوي الملف على تعليمات لجلب ورقة أنماط CSS تعرّف أنماط التطبيق، وعلى وسم script واحد يوجّه المتصفح لجلب شيفرة JavaScript الخاصة بالتطبيق - أي تطبيق React الفعلي.

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

تبويب Network لتطبيق الملاحظات على الواجهة الخلفية

يبدو الإعداد الجاهز لنشر المنتج كما يلي:

مخطط تطبيق React الجاهز للنشر

على عكس تشغيل التطبيق في بيئة التطوير، أصبح كل شيء الآن في الواجهة الخلفية node/express نفسها التي تعمل على localhost:3001. عندما ينتقل المتصفح إلى الصفحة، يُعرض ملف index.html. ويؤدي ذلك إلى جلب المتصفح لنسخة الإنتاج من تطبيق React. وبمجرد أن يبدأ بالعمل، يجلب بيانات json من العنوان localhost:3001/api/notes.

نقل التطبيق كاملاً إلى الإنترنت

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

في حالة Fly.io يتم النشر الجديد بالأمر

fly deploy

ملاحظة: يسرد ملف .dockerignore في مجلد مشروعك الملفات التي لا تُرفع أثناء النشر. قد يكون مجلد dist مضمنًا افتراضياً. إذا كان الأمر كذلك، فأزل الإشارة إليه من ملف .dockerignore، لضمان نشر تطبيقك بشكل صحيح.

في حالة Render، اعمل commit للتغييرات، وادفع الشيفرة إلى GitHub مرة أخرى. تأكد من أن مجلد dist غير متجاهَل من git في الواجهة الخلفية. قد يكفي الدفع إلى GitHub. وإذا لم يعمل النشر التلقائي، فاختر "النشر اليدوي" من لوحة تحكم Render.

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

لقطة شاشة لتطبيق الملاحظات

ملاحظة: تغيير الأهمية لا يعمل بعد لأن الواجهة الخلفية لا تملك تنفيذاً له حتى الآن.

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

يحتاج التطبيق إلى قاعدة بيانات. قبل أن نقدّم واحدة، لنستعرض بضعة أمور.

يبدو الإعداد الآن كما يلي:

مخطط تطبيق React على fly.io

تقيم الواجهة الخلفية node/express الآن على خادم Fly.io/Render. عند الوصول إلى العنوان الجذر، يُحمّل المتصفح تطبيق React وينفّذه، فيجلب بيانات json من خادم Fly.io/Render.

تبسيط نشر الواجهة الأمامية

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

سكربت Fly.io

تبدو السكربتات كما يلي:

{
  "scripts": {
    // ...
    "build:ui": "rm -rf dist && cd ../notes-frontend/ && npm run build && cp -r dist ../notes-backend",
    "deploy": "fly deploy",
    "deploy:full": "npm run build:ui && npm run deploy",    
    "logs:prod": "fly logs"
  }
}

يبني السكربت npm run build:ui الواجهة الأمامية وينسخ نسخة الإنتاج إلى مستودع الواجهة الخلفية. ويطلق السكربت npm run deploy الواجهة الخلفية الحالية إلى Fly.io.

يجمع npm run deploy:full هذين السكربتين، أي npm run build:ui و_npm run deploy_.

يوجد أيضاً سكربت npm run logs:prod لعرض سجلات Fly.io.

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

ملاحظة لمستخدمي Windows

لاحظ أن أوامر الصدفة (shell) القياسية في build:ui لا تعمل أصلاً في Windows. يعمل Powershell في Windows بشكل مختلف، وفي هذه الحالة يمكن كتابة السكربت كما يلي

"build:ui": "@powershell Remove-Item -Recurse -Force dist && cd ../frontend && npm run build && @powershell Copy-Item dist -Recurse ../backend",

إذا لم يعمل السكربت على Windows، فتأكد من أنك تستخدم Powershell وليس Command Prompt. وإذا كنت قد ثبّت Git Bash أو طرفية أخرى شبيهة بـ Linux، فقد تتمكن من تشغيل أوامر شبيهة بأوامر Linux على Windows أيضاً.

Render

ملاحظة: عندما تحاول نشر واجهتك الخلفية على Render، تأكد من أن لديك مستودعاً منفصلاً للواجهة الخلفية وانشر مستودع GitHub هذا عبر Render؛ فمحاولة النشر عبر مستودع Fullstackopen الخاص بك كثيراً ما تُطلق الخطأ "ERR path ....package.json".

في حالة Render، تبدو السكربتات كما يلي

{
  "scripts": {
    //...
    "build:ui": "rm -rf dist && cd ../frontend && npm run build && cp -r dist ../backend",
    "deploy:full": "npm run build:ui && git add . && git commit -m uibuild && git push"
  }
}

يبني السكربت npm run build:ui الواجهة الأمامية وينسخ نسخة الإنتاج إلى مستودع الواجهة الخلفية. ويحتوي npm run deploy:full أيضاً على أوامر git اللازمة لتحديث مستودع الواجهة الخلفية.

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

ملاحظة على Windows، تُنفَّذ سكربتات npm في cmd.exe كصدفة افتراضية، وهي لا تدعم أوامر bash. لكي تعمل أوامر bash أعلاه، يمكنك تغيير الصدفة الافتراضية إلى Bash (في التثبيت الافتراضي لـ Git for Windows) كما يلي:

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

خيار آخر هو استخدام shx.

الوكيل

أدّت التغييرات في الواجهة الأمامية إلى توقف عملها في وضع التطوير (عند التشغيل بالأمر npm run dev)، إذ لا يعمل الاتصال بالواجهة الخلفية.

أدوات مطوري Network تُظهر خطأ 404 عند جلب الملاحظات

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

const baseUrl = '/api/notes'

لأن الواجهة الأمامية في وضع التطوير على العنوان localhost:5173، تذهب الطلبات إلى الواجهة الخلفية إلى العنوان الخاطئ localhost:5173/api/notes. بينما الواجهة الخلفية على localhost:3001.

إذا أُنشئ المشروع بـ Vite، فحل هذه المشكلة سهل. يكفي إضافة التعريف التالي إلى ملف vite.config.js في مجلد الواجهة الأمامية.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  // highlight-start
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
      },
    }
  },
  // highlight-end
})


بعد إعادة التشغيل، ستعمل بيئة تطوير React كوكيل. إذا أرسلت شيفرة React طلب HTTP إلى مسار يبدأ بـ http://localhost:5173/api، فسيُمرَّر الطلب إلى الخادم على http://localhost:3001. أما الطلبات إلى مسارات أخرى فسيتولاها خادم التطوير بشكل طبيعي.

الآن تعمل الواجهة الأمامية بشكل صحيح أيضاً. فهي تعمل في وضع التطوير ووضع الإنتاج مع الخادم. وبما أن جميع الطلبات من منظور الواجهة الأمامية تُرسَل إلى http://localhost:5173، وهو الأصل الوحيد، فلم تعد هناك حاجة إلى وسيط cors في الواجهة الخلفية. لذلك يمكننا إزالة الإشارات إلى مكتبة cors من ملف index.js في الواجهة الخلفية وإزالة cors من اعتماديات المشروع:

npm remove cors

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

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

تمارين 3.9.-3.11

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

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

اجعل الواجهة الخلفية تعمل مع واجهة دليل الهاتف الأمامية من تمارين الجزء السابق. لا تنفّذ بعد وظيفة إجراء تغييرات على أرقام الهاتف، فهي ستُنفَّذ في التمرين 3.17.

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

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

انشر الواجهة الخلفية على الإنترنت، مثلاً على Fly.io أو Render. إذا كنت تستخدم Fly.io فينبغي تشغيل الأوامر في المجلد الجذر للواجهة الخلفية (أي في المجلد نفسه الذي يوجد فيه ملف package.json الخاص بالواجهة الخلفية).

نصيحة احترافية: عندما تنشر تطبيقك على الإنترنت، من المفيد على الأقل في البداية أن تراقب سجلات التطبيق في جميع الأوقات.

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

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

3.11: دليل الهاتف الكامل

ولّد بناء إنتاج لواجهتك الأمامية، وأضفه إلى تطبيق الإنترنت بالطريقة المقدَّمة في هذا الجزء.

تأكد أيضاً من أن الواجهة الأمامية لا تزال تعمل محلياً (في وضع التطوير عند التشغيل بالأمر npm run dev).

إذا كنت تستخدم Render، فتأكد من أن مجلد dist غير متجاهَل من git في الواجهة الخلفية.

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