مواضيع متنوعة
مكوّنات الصف
خلال المقرر، استخدمنا فقط مكوّنات React المعرَّفة كدوال JavaScript. لم يكن ذلك ممكناً بدون وظيفة الخطافات (hooks) التي جاءت مع الإصدار 16.8 من React، والذي صدر في 6 فبراير 2019. قبل ذلك، عند تعريف مكوّن يستخدم الحالة، كان على المرء تعريفه باستخدام صيغة الصف (Class) في JavaScript.
من المفيد أن تكون على دراية بمكوّنات الصف (Class Components) ولو إلى حد ما، لأن العالم لا يزال يحتوي على الكثير من شيفرة React القديمة، التي لن تُعاد كتابتها بالكامل على الأرجح باستخدام الصيغة المحدَّثة.
لنتعرّف على الميزات الرئيسية لمكوّنات الصف من خلال إنتاج تطبيق الطرائف المألوف جداً مرة أخرى. نخزّن الطرائف في الملف db.json باستخدام json-server. محتوى الملف مأخوذ من هنا.
النسخة الأولية من مكوّن الصف تبدو هكذا
import React from 'react'
class App extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<div>
<h1>anecdote of the day</h1>
</div>
)
}
}
export default App
أصبح للمكوّن الآن مُنشئ (constructor)، لا يحدث فيه شيء في الوقت الحالي، ويحتوي على الدالة render. وكما قد يتوقّع المرء، يحدّد render كيف وماذا يُعرض على الشاشة.
لنعرّف حالة لقائمة الطرائف والطريفة المعروضة حالياً. وعلى عكس استخدام خطاف useState ، تحتوي مكوّنات الصف على حالة واحدة فقط. فإذا كانت الحالة مكوّنة من عدة «أجزاء»، فينبغي تخزينها كخصائص للحالة. تُهيَّأ الحالة في المُنشئ:
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
anecdotes: [],
current: 0
}
}
render() {
if (this.state.anecdotes.length === 0) {
return <div>no anecdotes...</div>
}
return (
<div>
<h1>anecdote of the day</h1>
<div>
{this.state.anecdotes[this.state.current].content}
</div>
<button>next</button>
</div>
)
}
}
حالة المكوّن موجودة في متغير النسخة this.state. الحالة كائن له خاصيتان. this.state.anecdotes هي قائمة الطرائف وthis.state.current هو فهرس الطريفة المعروضة حالياً.
قد تتساءل عمّا تشير إليه this في الشيفرة. في JavaScript، تعتمد قيمة this على كيفية استدعاء الدالة. وداخل دالة صف كهذه، تشير إلى نسخة المكوّن، ما يتيح الوصول إلى حالة النسخة ودوالها. اقرأ المزيد هنا.
في المكوّنات الدالية، المكان الصحيح لجلب البيانات من الخادم هو داخل خطاف التأثير (effect hook)، الذي يُنفَّذ عند عرض المكوّن أو بوتيرة أقل عند الحاجة، مثلاً فقط مع العرض الأول.
تقدّم دوال دورة الحياة (lifecycle methods) في مكوّنات الصف وظيفة مماثلة. والمكان الصحيح لبدء جلب البيانات من الخادم هو داخل دالة دورة الحياة componentDidMount، التي تُنفَّذ مرة واحدة مباشرة بعد أول عرض للمكوّن:
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
anecdotes: [],
current: 0
}
}
componentDidMount = () => {
axios.get('http://localhost:3001/anecdotes').then(response => {
this.setState({ anecdotes: response.data })
})
}
// ...
}
تحدّث الدالة الرديفة لطلب HTTP حالة المكوّن باستخدام الدالة setState. ولا تلمس الدالة إلا المفاتيح المعرَّفة في الكائن الممرَّر إليها كمعامل. وتبقى قيمة المفتاح current دون تغيير.
استدعاء الدالة setState يؤدي دائماً إلى إعادة عرض مكوّن الصف، أي استدعاء الدالة render.
سننهي المكوّن بإمكانية تغيير الطريفة المعروضة. فيما يلي شيفرة المكوّن بأكمله مع تمييز الإضافة:
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
anecdotes: [],
current: 0
}
}
componentDidMount = () => {
axios.get('http://localhost:3001/anecdotes').then(response => {
this.setState({ anecdotes: response.data })
})
}
handleClick = () => {
const current = Math.floor(
Math.random() * this.state.anecdotes.length
)
this.setState({ current })
}
render() {
if (this.state.anecdotes.length === 0 ) {
return <div>no anecdotes...</div>
}
return (
<div>
<h1>anecdote of the day</h1>
<div>{this.state.anecdotes[this.state.current].content}</div>
<button onClick={this.handleClick}>next</button>
</div>
)
}
}
للمقارنة، إليك التطبيق نفسه كمكوّن دالي:
const App = () => {
const [anecdotes, setAnecdotes] = useState([])
const [current, setCurrent] = useState(0)
useEffect(() =>{
axios.get('http://localhost:3001/anecdotes').then(response => {
setAnecdotes(response.data)
})
},[])
const handleClick = () => {
setCurrent(Math.round(Math.random() * (anecdotes.length - 1)))
}
if (anecdotes.length === 0) {
return <div>no anecdotes...</div>
}
return (
<div>
<h1>anecdote of the day</h1>
<div>{anecdotes[current].content}</div>
<button onClick={handleClick}>next</button>
</div>
)
}
في حالة مثالنا، كانت الاختلافات طفيفة. وأكبر فرق بين المكوّنات الدالية ومكوّنات الصف هو أساساً أن حالة مكوّن الصف كائن واحد، وأن الحالة تُحدَّث باستخدام الدالة setState، بينما في المكوّنات الدالية يمكن أن تتكوّن الحالة من عدة متغيرات مختلفة، ولكل منها دالة تحديث خاصة بها.
في عام 2026، أصبحت مكوّنات الصف إلى حد كبير أثراً تاريخياً. فكل تطوير React الحديث يستخدم المكوّنات الدالية مع الخطافات، ولا يوجد سبب عقلاني للجوء إلى مكوّن صف عند كتابة شيفرة جديدة. وحتى توثيق React نفسه يتعامل مع مكوّنات الصف كواجهة برمجية قديمة.
حدّ الأخطاء
رغم أن مكوّنات الصف أصبحت شبه مهجورة، هناك حالة واحدة لا يزال لا يمكنك تجنّبها فيها: حدود الأخطاء (error boundaries). حدّ الأخطاء مكوّن يلتقط أخطاء JavaScript في أي مكان في شجرة مكوّناته الفرعية ويعرض واجهة بديلة بدلاً من انهيار التطبيق بأكمله. وحتى عام 2026، لم يقدّم React بعد بديلاً قائماً على الخطافات لهذا، لذا لا تزال حدود الأخطاء تُنفَّذ كمكوّنات صف.
يبدو حدّ الأخطاء هكذا:
import React from 'react'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, info) {
console.error('ErrorBoundary caught an error', error, info)
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong.</h2>
<p>{this.state.error.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
try again
</button>
</div>
)
}
return this.props.children
}
}
export default ErrorBoundary
دالتا دورة الحياة الأساسيتان هما getDerivedStateFromError، التي تحدّث الحالة كي يعرض العرض التالي الواجهة البديلة، وcomponentDidCatch، وهي مكان جيد لتسجيل الخطأ لدى خدمة الإبلاغ عن الأخطاء.
يمكنك لفّ أي جزء من شجرة مكوّناتك بحدّ أخطاء لحصر الأعطال في تلك الشجرة الفرعية:
const App = () => {
return (
<div>
<ErrorBoundary>
<Notes />
</ErrorBoundary>
<ErrorBoundary>
<Persons />
</ErrorBoundary>
</div>
)
}
إذا أطلقت Notes خطأً، يُعرض البديل في ذلك القسم فقط. وتستمر Persons في العمل بشكل طبيعي.
ولأن هذه هي حالة الاستخدام الوحيدة المتبقية لمكوّنات الصف، تستخدم مشاريع كثيرة مكتبة react-error-boundary التي تغلّف الآلية القائمة على الصفوف خلف واجهة برمجية مريحة لمكوّن دالي، فلا تضطر أبداً إلى كتابة مكوّن صف بنفسك.
الواجهة الأمامية والواجهة الخلفية في المستودع نفسه
خلال المقرر، أنشأنا الواجهة الأمامية والواجهة الخلفية كمستودعين منفصلين. غير أننا أجرينا النشر بنسخ شيفرة الواجهة الأمامية المجمَّعة إلى مستودع الواجهة الخلفية. وكان من الممكن أن يكون النهج الأفضل هو نشر شيفرة الواجهة الأمامية بشكل منفصل.
في كثير من الأحيان يُوضع التطبيق بأكمله في مستودع واحد. ومن الطرق الشائعة والنظيفة لفعل ذلك باستخدام مجموعة تقنيات حديثة إبقاء واجهة Vite الأمامية في مجلد client والواجهة الخلفية Express في مجلد server، ولكل منهما package.json خاص به. ويحصل جذر المستودع على package.json ثالث يعمل كغلاف مريح فيه سكربتات لتشغيل الاثنين معاً.
التخطيط الأدنى لمثل هذا المستودع يبدو هكذا:
app/
package.json (root, scripts only)
client/
package.json (Vite + React)
vite.config.js
src/
App.jsx
server/
package.json (Express)
index.js
خادم Express في server/index.js يقدّم الـ API، وفي الإنتاج يقدّم أيضاً الواجهة الأمامية المبنية من مجلد client/dist:
const express = require('express')
const path = require('path')
const app = express()
app.use(express.json())
app.get('/api/ping', (req, res) => {
res.json({ message: 'pong', time: new Date().toISOString() })
})
// تقديم الواجهة الأمامية المبنية بـ Vite في الإنتاج
if (process.env.NODE_ENV === 'production') {
app.use(express.static(path.join(__dirname, '../client/dist')))
app.get('/*splat', (req, res) => {
res.sendFile(path.join(__dirname, '../client/dist/index.html'))
})
}
const PORT = process.env.PORT || 3001
app.listen(PORT, () => console.log(`server running on port ${PORT}`))
ماذا عن /*splat
كتابة app.get('/*splat', handler) تُنشئ مساراً شاملاً يطابق أي مسار (مثل /، و/about، و/foo/bar).
يهم هذا المسار تطبيق React يستخدم موجّهاً (مثل React Router): فعندما يعيد المستخدم تحميل صفحة أو يفتح رابطاً مثل /notes أو /notes/5 مباشرةً، يذهب ذلك الطلب إلى الخادم أولاً وليس إلى React. وبدون هذا المسار الشامل، سيعيد Express استجابة 404 لعدم وجود مسار كهذا. وبإرسال index.html لكل مسار غير مطابق بدلاً من ذلك، يُحمَّل React ويتولّى موجّهه الأمر، فيعرض الصفحة الصحيحة من جهة العميل.
أثناء التطوير، يعمل خادم Vite التطويري على منفذه الخاص ويحتاج إلى تمرير طلبات الـ API إلى Express. ويُضبط ذلك في client/vite.config.js:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:3001',
},
},
})
مع وجود الوسيط (proxy)، يُمرَّر أي جلب من الواجهة الأمامية إلى /api/ping تلقائياً إلى خادم Express أثناء التطوير، فلا تضطر أبداً إلى ترميز عنوان الواجهة الخلفية بشكل ثابت.
يربط ملف package.json الجذري كل شيء معاً بسكربتين:
{
"scripts": {
"dev": "concurrently \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
"build": "npm run build --prefix client",
"start": "NODE_ENV=production npm start --prefix server"
},
"devDependencies": {
"concurrently": "^8.0.0"
}
}
هناك بضعة أمور مثيرة للاهتمام هنا.
يستخدم سكربت dev concurrently، وهي أداة مساعدة صغيرة تشغّل عدة أوامر في الوقت نفسه وتدمج مخرجاتها في تدفق واحد في الطرفية. وبدونها سيكون عليك فتح طرفيتين منفصلتين، واحدة للواجهة الخلفية وواحدة للواجهة الأمامية.
تخبر راية --prefix npm أي مجلد فرعي يجب اعتباره مجلد العمل لذلك الأمر، لذا فإن npm run dev --prefix server مكافئ لـ cd server && npm run dev.
تشغيل npm run dev من الجذر يبدأ بالتالي كلاً من خادم Vite التطويري وExpress بالتوازي بأمر واحد. وفي هذا النمط، تقدّم Vite الواجهة الأمامية مع استبدال الوحدات الساخن: فعند تعديل مكوّن React، يتحدّث المتصفح فوراً دون إعادة تحميل كاملة للصفحة. ويعمل خادم Express بشكل منفصل، ويحوّل وسيط Vite طلبات /api إليه.
تشغيل npm run build يجمّع الواجهة الأمامية في مجلد client/dist. وبعد ذلك، يضبط npm start القيمة NODE_ENV=production ويبدأ Express، الذي يلتقط الملفات الثابتة من client/dist ويقدّم الـ API والواجهة الأمامية معاً من منفذ واحد. هذا هو الإعداد الذي ستستخدمه عند النشر على خادم.
ولأن لكل جزء من المشروع ملف package.json خاصاً به، عليك أن تكون صريحاً بشأن أيها تستهدف عند تثبيت حزم جديدة. وتعمل الراية --prefix نفسها مع npm install أيضاً:
npm install axios --prefix client # إضافة إلى الواجهة الأمامية
npm install mongoose --prefix server # إضافة إلى الواجهة الخلفية
بدلاً من ذلك، يمكنك ببساطة تنفيذ cd إلى المجلد وتشغيل npm install من هناك كما تفعل عادةً.
تنظيم الشيفرة في تطبيق React
في معظم التطبيقات خلال هذا المقرر، اتّبعنا اصطلاح وضع المكوّنات في مجلد components، والخطافات في hooks، وشيفرة التواصل مع الخادم في services. وبالنسبة لتطبيق BlogList قد يبدو ذلك هكذا:
src/
App.jsx
components/
Blog.jsx
BlogList.jsx
LoginForm.jsx
Notification.jsx
hooks/
useField.js
services/
blogs.js
users.js
stores/
blogStore.js
notificationStore.js
هذا التجميع المسطّح القائم على النوع يعمل جيداً مع التطبيقات الصغيرة.
وعندما يستخدم التطبيق التوجيه، من الشائع إضافة مجلد pages (يُسمى أحياناً views) لمكوّنات المسارات العلوية، مع إبقاء مكوّنات واجهة المستخدم القابلة لإعادة الاستخدام في components. ويُستخدم هذا الاصطلاح في أطر مثل Next.js وهو موصوف في الأسئلة الشائعة في React حول بنية الملفات:
src/
App.jsx
pages/
HomePage.jsx
BlogPage.jsx
UserPage.jsx
components/
Blog.jsx
BlogList.jsx
LoginForm.jsx
Notification.jsx
hooks/
useField.js
services/
blogs.js
users.js
stores/
blogStore.js
notificationStore.js
مع نمو قاعدة الشيفرة أكثر، قد يظل التغيير في ميزة واحدة يلامس ملفات موزّعة في كل مجلد، وقد يصبح التنقّل في كلٍّ من components وpages صعباً.
ومن الاستجابات الشائعة لذلك تجميع الملفات حسب الميزة بدلاً من ذلك. وتُقعّد منهجية Feature-Sliced Design هذا النهج، ومشروع bulletproof-react مثال مرجعي على نطاق واسع لتطبيقه عملياً:
src/
App.jsx
features/
blogs/
Blog.jsx
BlogList.jsx
blogService.js
blogStore.js
users/
UserList.jsx
userService.js
notifications/
Notification.jsx
notificationStore.js
hooks/
useField.js
كل ما يتعلق بالمدونات يعيش معاً، لذا فإن إضافة ميزة أو تغييرها يعني العمل في مكان واحد بدلاً من عدة أماكن. ولا توجد طريقة صحيحة وحيدة لتنظيم مشروع أكبر، ويعتمد الخيار الصحيح على حجم التطبيق وطبيعته.
التغييرات على الخادم
التطبيقات التي نبنيها خلال هذا المقرر تجلب البيانات من الخادم عند تحميل الصفحة وبعد إجراءات المستخدم، لكنها لا تملك أي وسيلة لمعرفة التغييرات التي يجريها مستخدمون آخرون. فإذا أضاف مستخدم زميل تدوينة جديدة، فإن واجهتنا الأمامية ببساطة لا تعلم بذلك حتى تُحدَّث الصفحة. فكيف يمكننا إبقاء واجهة المستخدم متزامنة مع خادم يتغير باستقلالية؟
أبسط نهج هو الاستقصاء الدوري (polling): تسأل الواجهة الأمامية الخادم مراراً وتكراراً عن بيانات حديثة بفاصل زمني ثابت، مثلاً باستخدام setInterval. والاستقصاء الدوري سهل التنفيذ لكنه مضيِّع، لأن معظم الطلبات لا تعيد شيئاً جديداً.
البديل الأنظف هو WebSockets، التي تفتح اتصالاً ثنائي الاتجاه دائم بين المتصفح والخادم. ويمكن للخادم حينئذٍ دفع التحديثات إلى العملاء المتصلين لحظة حدوث تغيير، دون أن يضطر العميل إلى السؤال. وقد صارت WebSockets مدعومة الآن في جميع المتصفحات الحديثة.
قد يكون العمل مباشرة مع WebSocket API مرهقاً. وتغلّفه مكتبة Socket.io بواجهة برمجية أعلى مستوى وتضيف إعادة الاتصال التلقائي ووسائل راحة أخرى.
في الجزء 8 ننظر في GraphQL، الذي يتضمن آلية اشتراكات (subscriptions) تتيح للخادم إعلام العملاء بتغييرات البيانات بطريقة منظمة.
أمن تطبيقات React/node
حتى الآن خلال المقرر، لم نتطرق كثيراً إلى أمن المعلومات. وليس لدينا وقت كبير لذلك الآن أيضاً، لكن لحسن الحظ، لدى جامعة هلسنكي مقرر مفتوح عبر الإنترنت بعنوان تأمين البرمجيات (Securing Software) لهذا الموضوع المهم.
مع ذلك، سنلقي نظرة على بعض الأمور الخاصة بهذا المقرر.
ينشر مشروع أمن تطبيقات الويب المفتوحة، المعروف اختصاراً بـ OWASP، قائمة سنوية بأكثر مخاطر الأمن شيوعاً في تطبيقات الويب. ويمكن العثور على أحدث قائمة هنا. وتظهر المخاطر نفسها من سنة إلى أخرى.
في صدارة القائمة نجد الحقن (injection)، ويعني أن نصاً مثلاً يُرسَل باستخدام نموذج في تطبيق يُفسَّر بشكل مختلف تماماً عمّا قصده مطوّر البرمجيات. وأشهر أنواع الحقن هو على الأرجح حقن SQL.
على سبيل المثال، تخيّل أن استعلام SQL التالي يُنفَّذ في تطبيق قابل للاستغلال:
let query = "SELECT * FROM Users WHERE name = '" + userName + "';"
الآن لنفترض أن مستخدماً خبيثاً اسمه Arto Hellas عرّف اسمه على النحو التالي
Arto Hell-as'; DROP TABLE Users; --
بحيث يحتوي الاسم على علامة اقتباس مفردة '، وهي حرف البداية والنهاية لنص SQL. ونتيجة لذلك، ستُنفَّذ عمليتا SQL، والثانية منهما ستدمّر جدول قاعدة البيانات Users:
SELECT * FROM Users WHERE name = 'Arto Hell-as'; DROP TABLE Users; --'
يُمنع حقن SQL باستخدام الاستعلامات المُعاملة (parameterized queries). فمعها لا يُخلط إدخال المستخدم مع استعلام SQL، بل تُدرج قاعدة البيانات نفسها قيم الإدخال عند العناصر النائبة في الاستعلام (عادةً ?):
execute("SELECT * FROM Users WHERE name = ?", [userName])
هجمات الحقن ممكنة أيضاً في قواعد بيانات NoSQL. غير أن Mongoose يمنعها عبر تنقية الاستعلامات. ويمكن العثور على المزيد حول الموضوع مثلاً هنا.
البرمجة عبر المواقع (Cross-site scripting - XSS) هجوم يمكن فيه حقن شيفرة JavaScript خبيثة في تطبيق ويب شرعي. ثم تُنفَّذ الشيفرة الخبيثة في متصفح الضحية. وإذا حاولنا حقن ما يلي مثلاً في تطبيق الملاحظات:
<script>
alert('Evil XSS attack')
</script>
لا تُنفَّذ الشيفرة، بل تُعرض فقط كنص على الصفحة:

لأن React يعتني بتنقية البيانات في المتغيرات. وقد كانت بعض إصدارات React عرضة لهجمات XSS. وقد رُقّعت الثغرات الأمنية بالطبع، لكن لا ضمان لعدم وجود المزيد منها.
يجب أن يبقى المرء يقظاً عند استخدام المكتبات؛ فإذا وُجدت تحديثات أمنية لتلك المكتبات، فمن المستحسن تحديثها في تطبيقاته. وتوجد التحديثات الأمنية لـ Express في توثيق المكتبة وللـ Node في هذه المدونة.
يمكنك التحقق من مدى حداثة اعتمادياتك باستخدام الأمر
npm outdated --depth 0
المشروع الذي عمره سنة والمستخدم في الجزء 9 من هذا المقرر لديه بالفعل عدد لا بأس به من الاعتماديات القديمة:

يمكن تحديث الاعتماديات بتحرير الملف package.json. وأفضل طريقة لفعل ذلك استخدام أداة تسمى npm-check-updates. ويمكن تثبيتها عالمياً بتنفيذ الأمر:
npm install -g npm-check-updates
باستخدام هذه الأداة، يُفحص مدى حداثة الاعتماديات بالطريقة التالية:
$ npm-check-updates
Checking ...\my-app\package.json
[====================] 11/11 100%
@testing-library/react ^14.0.0 → ^15.0.0
@testing-library/user-event ^14.4.3 → ^14.5.2
react ^18.2.0 → ^19.0.0
vite ^5.0.0 → ^6.0.0
Run ncu -u to upgrade package.json
يُحدَّث الملف package.json بتنفيذ الأمر ncu -u.
$ ncu -u
Upgrading ...\my-app\package.json
[====================] 11/11 100%
@testing-library/react ^14.0.0 → ^15.0.0
@testing-library/user-event ^14.4.3 → ^14.5.2
react ^18.2.0 → ^19.0.0
vite ^5.0.0 → ^6.0.0
Run npm install to install new versions.
ثم يحين وقت تحديث الاعتماديات بتنفيذ الأمر npm install. غير أن الإصدارات القديمة من الاعتماديات ليست بالضرورة خطراً أمنياً.
يمكن استخدام أمر npm audit للفحص الأمني للاعتماديات. فهو يقارن أرقام إصدارات الاعتماديات في تطبيقك بقائمة أرقام إصدارات الاعتماديات التي تحتوي على تهديدات أمنية معروفة في قاعدة بيانات مركزية للأخطاء.
وعند تشغيل npm audit على المشروع نفسه، يطبع قائمة طويلة من الشكاوى والإصلاحات المقترحة. وفيما يلي جزء من التقرير:
$ patientor npm audit
... many lines removed ...
url-parse <1.5.2
Severity: moderate
Open redirect in url-parse - https://github.com/advisories/GHSA-hh27-ffr2-f2jc
fix available via `npm audit fix`
node_modules/url-parse
ws 6.0.0 - 6.2.1 || 7.0.0 - 7.4.5
Severity: moderate
ReDoS in Sec-Websocket-Protocol header - https://github.com/advisories/GHSA-6fc8-4gx4-v693
ReDoS in Sec-Websocket-Protocol header - https://github.com/advisories/GHSA-6fc8-4gx4-v693
fix available via `npm audit fix`
node_modules/webpack-dev-server/node_modules/ws
node_modules/ws
120 vulnerabilities (102 moderate, 16 high, 2 critical)
To address issues that do not require attention, run:
npm audit fix
To address all issues (including breaking changes), run:
npm audit fix --force
بعد سنة واحدة فقط، أصبحت الشيفرة مليئة بتهديدات أمنية صغيرة. ولحسن الحظ، لا يوجد سوى تهديدين حرجين. لنشغّل npm audit fix كما يقترح التقرير:
$ npm audit fix
+ mongoose@5.9.1
added 19 packages from 8 contributors, removed 8 packages and updated 15 packages in 7.325s
fixed 354 of 416 vulnerabilities in 20047 scanned packages
1 package update for 62 vulns involved breaking changes
(use `npm audit fix --force` to install breaking changes; or refer to `npm audit` for steps to fix these manually)
يتبقى 62 تهديداً لأن audit fix لا يحدّث افتراضياً الاعتماديات إذا زاد رقم إصدارها الرئيسي (major). وتحديث هذه الاعتماديات قد يؤدي إلى انهيار التطبيق بأكمله.
مصدر العلّة الحرجة هي مكتبة immer
immer <9.0.6
Severity: critical
Prototype Pollution in immer - https://github.com/advisories/GHSA-33f9-j839-rf8h
fix available via `npm audit fix --force`
Will install react-scripts@5.0.0, which is a breaking change
تشغيل npm audit fix --force سيُرقّي إصدار المكتبة لكنه سيرقّي أيضاً المكتبة react-scripts وقد يُعطّل ذلك بيئة التطوير. لذا سنترك ترقيات المكتبات لوقت لاحق...
يمكن لمشروع Node/React حديث أن يعتمد بسهولة، مباشرةً وبشكل متعدٍّ (transitively)، على مئات أو حتى آلاف الحزم المنشورة من أشخاص لم يقابلهم المطوّر قط. وهذا بالضبط ما يجعل هجمات سلسلة التوريد ممكنة: فبدلاً من مهاجمة تطبيق مباشرةً، يخترق المهاجم إحدى الاعتماديات التي يعتمد عليها، ثم تُسحب الشيفرة الخبيثة إلى كل مشروع يثبّت تلك الاعتمادية.
هناك عدة طرق شائعة لحدوث ذلك:
- يُختطف حساب أحد المشرفين على npm (مثلاً عبر بريد تصيّد أو رمز وصول مسرَّب غير محمي) وتُنشر نسخة خبيثة من حزمة موثوقة في الأصل
- يُعدَّل سكربت postinstall لحزمة أو الشيفرة نفسها ليسرق بهدوء متغيرات البيئة أو الرموز أو مفاتيح SSH أثناء npm install
- تُنشر حزمة خبيثة باسم يشبه عن قرب اسماً شائعاً، أملاً في أن يخطئ المطوّر في كتابة أمر فيثبّتها بالخطأ
- يعتمد مشروع على اعتمادية غير مُصانة يُنقل ملكيتها إلى مشرف جديد خبيث.
وبما أن شيفرة الاعتمادية المخترَقة تعمل بنفس صلاحيات بقية التطبيق (وغالباً، أثناء التثبيت، بصلاحيات جهاز المطوّر نفسه أو خط أنابيب CI)، فقد تكون العواقب وخيمة: سرقة بيانات الاعتماد، أو بناء نسخ إنتاجية بباب خلفي، أو تسريب بيانات المستخدمين.
بعض الطرق العملية لتقليل الخطر:
- أبقِ عدد الاعتماديات صغيراً قدر المعقول؛ فكل حزمة مضافة هي سطح هجوم إضافي.
- التزم بملف القفل (package-lock.json) واستخدم npm ci بدلاً من npm install في بيئات CI/الإنتاج، بحيث تُستخدم إصدارات الاعتماديات التي سبق التحقق منها بالضبط مع بصمات تكاملها.
- شغّل npm audit (أو ما يكافئه) بانتظام، ودع أدوات مثل Dependabot أو Renovate تفتح طلبات سحب تلقائياً عند إصدار نسخ جديدة.
- كن حذراً بشكل خاص عند إضافة اعتمادية جديدة تماماً: تحقق من مدى نشاط صيانتها، وكم مشروع آخر يعتمد عليها، وما إذا كان اسم الحزمة هو المقصود بالضبط.
- فكّر في تعطيل تنفيذ سكربتات التثبيت للاعتماديات التي لا تثق بها تماماً، مثلاً عبر npm install --ignore-scripts.
- تجنّب تثبيت إصدار حزمة لحظة نشره. فمعظم الإصدارات الخبيثة تُكتشف وتُسحب خلال الساعات أو الأيام الأولى، لذا تُصفّي مهلة قصيرة نسبة كبيرة منها. وبدءاً من إصدار npm 11.10.0، يمكن فرض ذلك عبر إعداد min-release-age في .npmrc (يُعطى بالأيام)، الذي يجعل npm يتجاهل أي إصدار ليس قديماً بما يكفي.
لا تجعل أي من هذه الخطوات التطبيق محصّناً ضد هجمات سلسلة التوريد، لكنها معاً تقلّص بشكل كبير النافذة التي يمكن فيها لاعتمادية مخترَقة إحداث الضرر قبل ملاحظتها.
من التهديدات المذكورة في قائمة OWASP المصادقة المعطوبة (Broken Authentication) وما يتصل بها التحكم المعطوب في الوصول (Broken Access Control). والمصادقة القائمة على الرموز التي كنا نستخدمها متينة إلى حد كبير إذا كان التطبيق يُستخدم عبر بروتوكول HTTPS المشفّر للحركة. وعند تنفيذ التحكم في الوصول، ينبغي للمرء مثلاً أن يتذكر عدم التحقق من هوية المستخدم في المتصفح فحسب بل على الخادم أيضاً. ومن سوء الأمن منع بعض الإجراءات فقط بإخفاء خيارات التنفيذ في شيفرة المتصفح.
على موقع MDN التابع لـ Mozilla، يوجد دليل أمن المواقع جيد جداً، يتناول هذا الموضوع المهم جداً:

يتضمن توثيق Express قسماً عن الأمن: أفضل ممارسات الإنتاج: الأمن، وهو جدير بالقراءة. ويُوصى أيضاً بإضافة مكتبة تسمى Helmet إلى الواجهة الخلفية. وهي تتضمن مجموعة من الوسائط (middleware) التي تُزيل بعض الثغرات الأمنية في تطبيقات Express.
استخدام إضافة الأمن في ESlint جدير بالعمل به أيضاً.
الاتجاهات الحالية
أخيراً، لنلقِ نظرة على بعض تقنيات الغد (أو، في الواقع، تقنيات اليوم بالفعل)، والاتجاهات التي يتجه إليها تطوير الويب.
النسخ المُنمَّطة من JavaScript
قد تؤدي الكتابة الديناميكية (dynamic typing) في JavaScript إلى أخطاء خفية لا تُكتشف إلا وقت التشغيل. وكان المقرر يغطي PropTypes كطريقة لإضافة فحوصات أنواع وقت التشغيل إلى props المكوّن، لكن PropTypes خرجت إلى حد كبير من الاستخدام مع تحرّك المنظومة نحو الفحص الساكن للأنواع (static type checking).
TypeScript، المطوَّر من Microsoft، أصبح المعيار الفعلي لـ JavaScript المُنمَّط. فهو يلتقط أخطاء الأنواع وقت الترجمة بدلاً من وقت التشغيل، ويوفر أدوات تحرير ممتازة، ويُستخدم الآن في غالبية مشاريع React الجديدة. ويُغطى TypeScript في الجزء 9.
العرض من الخادم ومكوّنات الخادم في React
لا يجب أن تعمل مكوّنات React في المتصفح. فيمكن أيضاً عرضها على الخادم، الذي يرسل HTML جاهزاً إلى العميل بدلاً من صفحة فارغة يجب على JavaScript أن يملأها. وهذا العرض من الخادم (server-side rendering) (SSR) يحسّن زمن التحميل المُدرَك ويهم لتحسين محركات البحث (SEO)، لأن زواحف محركات البحث ترى محتوى معروضاً بالكامل دون الحاجة إلى تنفيذ JavaScript.
التطور الأحدث والأهم هو مكوّنات الخادم في React (React Server Components) (RSC)، التي قُدّمت في React 18 وأصبحت الآن جزءاً أساسياً من معمارية React. يعمل مكوّن الخادم حصراً على الخادم ولا يُرسل أبداً إلى المتصفح كـ JavaScript. ويمكنه القراءة مباشرة من قاعدة بيانات أو نظام ملفات، وإبعاد الأسرار عن حزمة العميل، وبثّ مخرجاته إلى المتصفح. ويتلقى المتصفح هذه المكوّنات كبيانات معروضة، لا كشيفرة قابلة للتنفيذ. أما مكوّنات العميل (Client Components)، المعلَّمة بـ'use client'، لا تزال تعمل في المتصفح وتتعامل مع التفاعل كما في السابق. وفي تطبيق RSC، فمعظم المكوّنات هي مكوّنات خادم افتراضياً، وتُستخدم مكوّنات العميل فقط حيث يلزم تفاعل المستخدم.
Next.js أصبح الإطار المعياري لبناء تطبيقات React التي تتطلب سلوكاً من جهة الخادم. وموجّه App Router الخاص به (المقدَّم في Next.js 13) مبني حول مكوّنات الخادم في React ويوفر توجيهاً قائماً على الملفات، وتخطيطات متداخلة، وإجراءات خادم لتغيير البيانات، ودعماً مدمجاً للتوليد الساكن وإعادة التوليد الساكن التدريجي. وفي عام 2026، يُعدّ Next.js الخيار الأول لأي مشروع React يهم فيه SSR أو SEO أو القدرات كاملة الحزمة. ويغطي 14 الجزء 14 من المقرر Next.js.
معمارية الخدمات المصغّرة
خلال هذا المقرر، لم نلمس سوى سطح الجانب الخادمي. ففي تطبيقاتنا، كانت لدينا واجهة خلفية أحادية (monolithic)، أي تطبيق واحد يشكّل كلاً متكاملاً ويعمل على خادم واحد، ويقدّم بضع نقاط نهاية API فقط.
ومع نمو التطبيق، يبدأ نهج الواجهة الخلفية الأحادية يسبب مشكلات من حيث الأداء وقابلية الصيانة معاً.
معمارية الخدمات المصغّرة (microservice architecture) (microservices) طريقة لتكوين الواجهة الخلفية لتطبيق من خدمات منفصلة ومستقلة كثيرة تتواصل مع بعضها عبر الشبكة. وغرض كل خدمة مصغّرة أن تتولى كلاً وظيفياً منطقياً بعينه. وفي معمارية الخدمات المصغّرة الخالصة، لا تستخدم الخدمات قاعدة بيانات مشتركة.
على سبيل المثال، يمكن أن يتكون تطبيق قائمة المدونات من خدمتين: واحدة تتعامل مع المستخدم وأخرى تتولى المدونات. وتكون مسؤولية خدمة المستخدم تسجيل المستخدم ومصادقته، بينما تتولى خدمة المدونات العمليات المتعلقة بالمدونات.
تُصوّر الصورة أدناه الفرق بين بنية تطبيق قائم على معمارية الخدمات المصغّرة وآخر قائم على بنية أحادية أكثر تقليدية:

دور الواجهة الأمامية (المحاط بمربع في الصورة) لا يختلف كثيراً بين النموذجين. وغالباً ما توجد بوابة API (API gateway) بين الخدمات المصغّرة والواجهة الأمامية، تمنح وهماً بواجهة برمجية أكثر تقليدية «كل شيء على الخادم نفسه». وتستخدم Netflix، بين آخرين، هذا النوع من النهج.
نشأت معماريات الخدمات المصغّرة وتطورت لتلبية احتياجات تطبيقات كبيرة على نطاق الإنترنت. وقد أرسى الاتجاه Amazon قبل ظهور مصطلح الخدمة المصغّرة بوقت طويل. وكانت نقطة البداية الحاسمة بريداً إلكترونياً أرسله الرئيس التنفيذي لـ Amazon جيف بيزوس إلى جميع الموظفين عام 2002:
من الآن فصاعداً، ستكشف جميع الفرق عن بياناتها ووظائفها عبر واجهات الخدمة.
يجب أن تتواصل الفرق مع بعضها عبر هذه الواجهات.
لن يُسمح بأي شكل آخر من التواصل بين العمليات: لا ربط مباشر، ولا قراءات مباشرة لمخزن بيانات فريق آخر، ولا نموذج ذاكرة مشتركة، ولا أبواب خلفية على الإطلاق. التواصل الوحيد المسموح هو عبر استدعاءات واجهة الخدمة عبر الشبكة.
لا يهم أي تقنية تستخدم.
يجب تصميم جميع واجهات الخدمة، دون استثناء، من الأساس لتكون قابلة للتجسيد الخارجي. أي أنه يجب على الفريق التخطيط والتصميم ليكون قادراً على كشف الواجهة للمطورين في العالم الخارجي.
لا استثناءات.
من لا يفعل هذا سيُطرد. شكراً لكم؛ أتمنى لكم يوماً سعيداً!
في أيامنا هذه، من أكبر الرواد في استخدام الخدمات المصغّرة Netflix.
ظل استخدام الخدمات المصغّرة يكتسب ضجة مطردة ليكون نوعاً من الرصاصة الفضية (silver bullet) في يومنا هذا، حيث يُقدَّم حلاً لكل نوع من المشكلات تقريباً. غير أن هناك تحديات عدة عند تطبيق معمارية الخدمات المصغّرة، وقد يكون من المنطقي البدء بالبنية الأحادية (monolith first) بإنشاء واجهة خلفية تقليدية شاملة أولاً. أو ربما لا. فهناك آراء مختلفة كثيرة في الموضوع. وكلا الرابطين يقودان إلى موقع مارتن فاولر؛ وكما نرى، حتى الحكماء ليسوا متأكدين تماماً أيّ الطرق الصحيحة أصح.
للأسف، لا يمكننا الغوص أعمق في هذا الموضوع المهم خلال هذا المقرر. فحتى نظرة عابرة على الموضوع تتطلب 5 أسابيع إضافية على الأقل.
بلا خادم (Serverless)
بعد إصدار خدمة lambda من Amazon في نهاية 2014، بدأ اتجاه جديد يظهر في تطوير تطبيقات الويب: serverless.
الأمر الرئيسي في lambda، وكذلك Cloud functions من Google اليوم ووظائف مشابهة في Azure، أنه يمكّن من تنفيذ دوال فردية في السحابة. وقبل ذلك، كانت أصغر وحدة قابلة للتنفيذ في السحابة عملية (process) واحدة، مثلاً بيئة تشغيل تشغّل واجهة خلفية Node.
مثلاً، باستخدام بوابة API من Amazon يمكن إنشاء تطبيقات بلا خادم حيث تحصل الطلبات إلى واجهة HTTP API المعرَّفة على استجابات مباشرة من دوال سحابية. وعادةً تعمل الدوال بالفعل باستخدام بيانات مخزنة في قواعد بيانات خدمة السحابة.
لا يتعلق Serverless بعدم وجود خادم في التطبيقات، بل بكيفية تعريف الخادم. فيمكن لمطوّري البرمجيات نقل جهودهم البرمجية إلى مستوى أعلى من التجريد إذ لم تعد هناك حاجة لتعريف توجيه طلبات HTTP وعلاقات قواعد البيانات وغير ذلك برمجياً، لأن البنية التحتية السحابية توفر كل هذا. كما أن الدوال السحابية تناسب إنشاء نظام جيد التوسع، فمثلاً يمكن لـ Lambda من Amazon تنفيذ عدد هائل من الدوال السحابية في الثانية. ويحدث كل هذا تلقائياً عبر البنية التحتية ولا حاجة إلى بدء خوادم جديدة وغير ذلك.
مكتبات مفيدة وقراءات إضافية
أنتج مجتمع مطوّري JavaScript تنوّعاً كبيراً من المكتبات المفيدة. وقبل كتابة شيء من الصفر، يجدر دائماً التحقق مما إذا كان هناك حل جيد الصيانة موجود بالفعل.
يمكنك الاستفادة من معرفتك بـ React عند تطوير تطبيقات الهواتف المحمولة باستخدام React Native، وهو موضوع الجزء 10 من المقرر.
ويستمر المقرر نفسه بعد الجزء 7: يغطي الجزء 8 GraphQL، والجزء 9 TypeScript، والجزء 10 React Native، والجزء 11 CI/CD، والجزء 12 الحاويات، والجزء 13 استخدام قواعد بيانات SQL، والجزء 14 Next.js. ومحتويات المقرر الكاملة مدرجة في صفحة المقرر.
المصادر الخارجية التالية أماكن جيدة للتعمق في أنماط React وجودة الشيفرة والمنظومة الأوسع:
- Patterns.dev يغطي أنماط React وJavaScript الحديثة بعمق. ولمجموعة منتقاة من التقنيات الخاصة بـ React، يُعدّ React bits رفيقاً مفيداً.
- Overreacted مدونة دان أبراموف، أحد أعضاء فريق React الأساسي الأصليين. وتتناول المقالات قرارات تصميم React ونماذجه الذهنية بعمق، وتستحق القراءة حتى وهي بعد سنوات.
- Kent C. Dodds يكتب بإسهاب عن أفضل ممارسات React والاختبار وتصميم المكوّنات. وقد شكّلت مقالاته عن فلسفة الاختبار على وجه الخصوص طريقة تفكير المجتمع في اختبارات الواجهة الأمامية.
- Tao of React دليل قصير ذو رأي واضح لبنية تطبيقات React يغطي المكوّنات والحالة وprops وتخطيط المشروع بطريقة عملية.
- Reactiflux مجتمع كبير لمطوّري React على Discord، ومكان جيد لطرح الأسئلة بعد انتهاء المقرر. وتحتفظ مكتبات مفتوحة المصدر كثيرة بقنواتها الخاصة هناك.