تطوير Full-Stack بـ Angular وNode.js وMySQL: 100 مثال من الصفر إلى الاحتراف
يتكون التطبيق متكامل الطبقات من ثلاثة أجزاء يجب أن تتفق مع بعضها: واجهة Angular التي يستخدمها الناس في المتصفح، وواجهة برمجية Node.js تطبّق القواعد، وقاعدة بيانات MySQL تحفظ البيانات بأمان. تبني هذه المقالة متجراً صغيراً من مجلد فارغ حتى إصدار يراعي بيئة الإنتاج عبر 100 مثال. ستجهّز المشروع، وتتعلم أساسيات Node.js وExpress لبناء واجهة REST، وتصمم جداول MySQL وتكتب استعلامات آمنة، وتربط الواجهة بقاعدة البيانات باستخدام المعاملات Transactions، وتتحقق من المدخلات، وتضيف تسجيل الدخول بكلمات مرور مشفّرة ورموز JWT، وتبني مكونات Angular والخدمات والمسارات والنماذج والمعترِضات Interceptors، وتربط كل شيء عبر الوكيل وملفات البيئة، وتختبر كل طبقة، وتجمّع النظام في Docker، وتجهّزه للإنتاج. ملاحظة صريحة: لم يكن MySQL ولا Angular CLI مثبتين على الحاسوب الذي أُعدّت به هذه المقالة، لذلك لم تُنفَّذ الأمثلة المئة هنا. تتبع الشيفرة الصياغة الحالية لـ Node.js وExpress وmysql2 وAngular وMySQL، لكن شغّل كل جزء بنفسك قبل الاعتماد عليه.
ما الذي تحتاجه
Node.js 20 أو أحدث مع npm، وMySQL 8.4 (أو Docker لتشغيله داخل حاوية)، وAngular CLI، ومحرر شيفرة مثل Visual Studio Code. تفيد معرفة أساسية بـ JavaScript وHTML. استخدم كلمات مرور نائبة مثل <strong-password> وأسماء نطاقات نائبة مثل shop.example.com، ولا ترفع ملف .env حقيقياً، ولا تلصق بيانات اعتماد حقيقية في الشيفرة أو الأوامر.
كيفية استخدام المثال
تستخدم أمثلة bash الأوامر npm وAngular CLI وDocker. تُكتب أمثلة JavaScript وTypeScript في ملفات مشروع Node.js وAngular المذكورة في النص. تعمل أمثلة SQL في MySQL 8.4، وتُكتب أمثلة HTML في قوالب Angular.
الموضوع 1: إعداد المشروع
ابدأ ببنية واضحة: مجلد للواجهة البرمجية، ومجلد لتطبيق Angular، ومجلد لسكربتات قاعدة البيانات. تجعل البنية الجيدة كل جزء سهل الاختبار والنشر والشرح لشخص جديد.
ثلاثة أجزاء في مستودع واحد
احتفظ بالواجهة البرمجية وتطبيق الويب وسكربتات قاعدة البيانات في مستودع واحد، حتى يحدّث تغيير واحد الأجزاء الثلاثة معاً. لكل جزء ملف حزم وأوامر خاصة به.
الأدوات والإصدارات
تحقق أولاً من Node.js وnpm وMySQL. يُنصح بـ Node.js 20 أو أحدث لأنه يتضمن مشغّل الاختبارات المدمج وfetch حديثاً. يُنشئ Angular CLI الشيفرة ويشغّل خادم التطوير.
الأسرار خارج Git
تُحفظ كلمات المرور ومفاتيح التوقيع في ملف .env مدرج في .gitignore. ارفع ملف مثال بقيم نائبة حتى يعرف الآخرون الإعدادات المطلوبة.
رسوم توضيحية
مثال 1: التحقق من الأدوات
تأكد من تثبيت Node.js وnpm وMySQL.
node --version
npm --version
mysql --version
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 2: إنشاء مجلدات المشروع
افصل الـ API وتطبيق الويب وقاعدة البيانات.
mkdir shop-fullstack && cd shop-fullstack
mkdir api web db
git init
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 3: إعداد واجهة Node.js API
ثبّت Express ومشغّل MySQL وdotenv.
cd api
npm init -y
npm install express mysql2 dotenv
npm install --save-dev nodemon
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 4: تثبيت Angular CLI
أنشئ واجهة Angular مع التوجيه.
npm install -g @angular/cli
cd ..
ng new web --routing --style=css --skip-git
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 5: هيكل المشروع
احتفظ بالأجزاء الثلاثة في مجلدات واضحة.
shop-fullstack/
api/ Node.js + Express + MySQL
web/ Angular front end
db/ SQL schema and seed data
docker-compose.yml
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 6: إبقاء الأسرار خارج Git
تجاهل .env وارفع ملف مثال بدلاً منه.
cat > .gitignore <<'EOF'
node_modules/
.env
dist/
EOF
cat > api/.env.example <<'EOF'
DB_HOST=localhost
DB_USER=shop
DB_PASSWORD=<strong-password>
DB_NAME=shop
PORT=3000
EOF
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 2: أساسيات Node.js
يشغّل Node.js الجافاسكربت خارج المتصفح. تتعلم الوحدات Modules والشيفرة غير المتزامنة ومتغيرات البيئة، إضافة إلى مجموعة صغيرة من الأفكار التي يستخدمها كل ملف خادم.
الوحدات والشيفرة غير المتزامنة
كل ملف وحدة تصدّر ما تحتاجه الملفات الأخرى. معظم عمل الخادم غير متزامن، لذلك استخدم Promise مع async وawait، وعالج الأخطاء بـ try وcatch حتى لا يُسقط فشل واحد العملية كلها.
الإعدادات من البيئة
اقرأ إعدادات مثل المنفذ وعنوان قاعدة البيانات من process.env. تحمّل حزمة dotenv ملف .env أثناء التطوير، أما خوادم الإنتاج فتتلقى المتغيرات عادة من المنصة.
السكربتات وحلقة الأحداث
تمنح سكربتات npm كل أعضاء الفريق الأوامر نفسها. تذكّر أن عملية متزامنة بطيئة واحدة تعطّل كل الطلبات، لذلك أبعد الأعمال الثقيلة عن معالجات الطلبات.
مثال 7: قراءة ملف بـ Node.js
القراءة المتزامنة للملفات وما تحمله من مقايضة.
const fs = require("node:fs");
const text = fs.readFileSync("notes.txt", "utf8");
console.log(text.split("\n").length, "lines");
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 8: القراءة غير المتزامنة للملفات
استخدم Promise مع async وawait.
const fs = require("node:fs/promises");
async function main() {
const text = await fs.readFile("notes.txt", "utf8");
console.log(text.trim());
}
main().catch(console.error);
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 9: الوحدات في Node.js
صدّر دالة واستوردها في ملف آخر.
// math.js
exports.add = (a, b) => a + b;
// main.js
const { add } = require("./math");
console.log(add(2, 3));
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 10: متغيرات البيئة
اقرأ الإعدادات من .env باستخدام dotenv.
require("dotenv").config();
const port = Number(process.env.PORT || 3000);
console.log("port", port);
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 11: المصفوفات والتصفية
صفِّ البيانات وحوّلها كما تفعل الواجهة البرمجية.
const orders = [
{ id: 1, total: 120 },
{ id: 2, total: 80 },
{ id: 3, total: 200 },
];
const big = orders.filter((o) => o.total >= 100).map((o) => o.id);
console.log(big); // [1, 3]
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 12: Promise وawait
انتظر العمل دون أن تعطل الخادم.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function run() {
console.log("start");
await wait(500);
console.log("done after 500 ms");
}
run();
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 13: الأخطاء بـ try وcatch
تعامل مع الفشل وأعد قيمة افتراضية آمنة.
async function readConfig(path) {
try {
const fs = require("node:fs/promises");
return JSON.parse(await fs.readFile(path, "utf8"));
} catch (err) {
console.error("could not read config:", err.message);
return {};
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 14: أوامر npm
شغّل الواجهة البرمجية وطوّرها واختبرها بأمر واحد.
{
"name": "api",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "node --test"
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 3: واجهات REST البرمجية مع Express
يربط Express طرق HTTP والمسارات بالدوال. تستخدم واجهة REST الطريقة GET للقراءة، وPOST للإنشاء، وPUT أو PATCH للتعديل، وDELETE للحذف، وتعيد JSON يمكن للواجهة الأمامية عرضه.
المسارات والمعاملات والأجسام
تطابق app.get وapp.post وأمثالها الطريقة والمسار. اقرأ معاملات الرابط من req.params، وسلسلة الاستعلام من req.query، وجسم JSON من req.body بعد تفعيل app.use(express.json()).
الموجّهات والوسيطات
يجمع الموجّه مسارات مورد واحد في ملف واحد. تُنفَّذ الوسيطات Middleware بالترتيب لكل طلب، لذلك هي المكان المناسب للتسجيل والمصادقة ومعالجة الأخطاء.
رموز حالة لها معنى
أعد 200 للقراءة، و201 عند إنشاء مورد، و204 عند الحذف بدون جسم، و400 لمدخلات غير صالحة، و401 عندما لا يكون المستخدم مسجّلاً، و403 عندما لا يُسمح له، و404 عندما لا يوجد العنصر.
مثال 15: أول خادم Express
أجب عن الطلب في بضعة أسطر.
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Shop API is running");
});
app.listen(3000, () => console.log("listening on http://localhost:3000"));
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 16: مسار يعيد JSON
أعد بيانات منظمة لواجهة الويب.
app.get("/api/health", (req, res) => {
res.json({ status: "ok", time: new Date().toISOString() });
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 17: معاملات المسار وسلسلة الاستعلام
اقرأ القيم من الرابط.
app.get("/api/products/:id", (req, res) => {
const id = Number(req.params.id);
const detail = req.query.detail === "1";
res.json({ id, detail });
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 18: تحليل جسم JSON
استقبل البيانات من نموذج أو من Angular.
app.use(express.json());
app.post("/api/products", (req, res) => {
const { name, price } = req.body;
res.status(201).json({ name, price });
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 19: تنظيم المسارات بموجّه
احفظ كل ميزة في ملف خاص بها.
// routes/products.js
const { Router } = require("express");
const router = Router();
router.get("/", (req, res) => res.json([]));
router.get("/:id", (req, res) => res.json({ id: req.params.id }));
module.exports = router;
// server.js
app.use("/api/products", require("./routes/products"));
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 20: وسيط تسجيل الطلبات
سجّل الطريقة والمسار والحالة والزمن.
app.use((req, res, next) => {
const started = Date.now();
res.on("finish", () => {
console.log(req.method, req.originalUrl, res.statusCode, Date.now() - started, "ms");
});
next();
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 21: معالج أخطاء مركزي
أعد صيغة خطأ واحدة من كل المسارات.
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).json({ error: "Something went wrong" });
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 22: 404 للمسارات غير المعروفة
أجب بوضوح عندما لا يوجد الرابط.
app.use((req, res) => {
res.status(404).json({ error: "Not found" });
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 4: أساسيات MySQL وSQL
يخزّن MySQL البيانات في جداول لها أعمدة بأنواع محددة ومفاتيح وعلاقات. تنشئ SQL الصفوف وتقرؤها وتعدّلها وتحذفها، والتصميم الجيد يمنع كثيراً من الأخطاء قبل تشغيل أي شيفرة.
قواعد البيانات والمستخدمون وأقل الصلاحيات
أنشئ قاعدة بيانات للمتجر ومستخدماً يستطيع القراءة والكتابة فيها فقط. لا تجعل التطبيق يتصل بحساب root أبداً، لأن كلمة مرور مسرّبة ستكشف الخادم كله.
الجداول والمفاتيح والأنواع
أعطِ كل جدول مفتاحاً أساسياً، واختر أنواعاً تطابق البيانات مثل DECIMAL للمال، واستخدم المفاتيح الأجنبية حتى لا يشير طلب إلى منتج غير موجود. استخدم UNIQUE للقيم التي لا يجب أن تتكرر مثل البريد الإلكتروني.
قراءة الصفوف وتعديلها
تعيد SELECT مع WHERE وORDER BY وLIMIT الصفوف التي تحتاجها. يجب أن تحتوي UPDATE وDELETE دائماً على شرط WHERE تختبره أولاً بـ SELECT، لأن الشرط الناقص يغيّر كل الصفوف.
مثال 23: إنشاء قاعدة البيانات والمستخدم
امنح التطبيق الصلاحيات اللازمة فقط.
CREATE DATABASE IF NOT EXISTS shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'shop'@'localhost' IDENTIFIED BY '<strong-password>';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop'@'localhost';
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 24: إنشاء جدول
عرّف الأعمدة والمفاتيح والقيم الافتراضية.
USE shop;
CREATE TABLE products (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(120) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 25: إدراج صفوف
أضف عدة صفوف في عبارة واحدة.
INSERT INTO products (name, price, stock)
VALUES ('Keyboard', 790.00, 12),
('Mouse', 350.00, 30),
('Monitor', 5490.00, 4);
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 26: SELECT مع تصفية وترتيب
اقرأ الصفوف المطلوبة فقط وبالترتيب.
SELECT id, name, price
FROM products
WHERE stock > 0
ORDER BY price DESC;
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 27: تحديث صف
غيّر القيم باستخدام شرط WHERE.
UPDATE products
SET price = 690.00
WHERE name = 'Keyboard';
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 28: حذف صف بأمان
اجعل الحذف محصوراً دائماً في الصف المقصود.
DELETE FROM products
WHERE id = 3
LIMIT 1;
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 29: الربط والتجميع
احسب مبيعات كل منتج باستخدام GROUP BY.
CREATE TABLE orders (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
product_id INT UNSIGNED NOT NULL,
quantity INT NOT NULL,
ordered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id)
);
SELECT p.name, SUM(o.quantity) AS sold
FROM orders o
JOIN products p ON p.id = o.product_id
GROUP BY p.name
ORDER BY sold DESC;
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 30: فهرسة عمود البحث
سرّع البحث وتحقق من الخطة باستخدام EXPLAIN.
CREATE INDEX idx_products_name ON products (name);
EXPLAIN SELECT * FROM products WHERE name = 'Mouse';
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 5: الوصول إلى البيانات من Node.js
تتحدث الواجهة البرمجية مع MySQL عبر مجمّع اتصالات Pool. تفصل الاستعلامات ذات المعاملات النائبة مدخلات المستخدم عن SQL، وتجمع المعاملات التغييرات المترابطة معاً، ويُبقي الترقيم الاستجابات صغيرة.
مجمّعات الاتصالات
يحتفظ المجمّع بمجموعة اتصالات مفتوحة ويعطي اتصالاً لكل طلب ثم يعيده. أنشئ المجمّع مرة واحدة عند البدء وأعد استخدامه، بدلاً من فتح اتصال جديد لكل طلب.
المعاملات النائبة تمنع حقن SQL
لا تدمج مدخلات المستخدم أبداً في نص SQL. اكتب ؟ في الاستعلام ومرر القيم في مصفوفة، فيقوم المشغّل بتهريبها. هذه العادة الواحدة تمنع واحدة من أكثر ثغرات الأمان شيوعاً على الويب.
المعاملات والترقيم
تجمع المعاملة عدة عبارات بحيث تنجح جميعها أو لا ينجح شيء منها، وهذا مهم عندما يُنقص الطلب المخزون وينشئ سجل طلب. استخدم LIMIT وOFFSET، أو الأفضل keyset، لإعادة صفحة واحدة في كل مرة.
مثال 31: مجمّع الاتصالات
أعد استخدام الاتصالات بدلاً من فتح اتصال لكل طلب.
const mysql = require("mysql2/promise");
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
});
module.exports = pool;
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 32: الاستعلام من Node.js
أرسل SQL من مسار وأعد الصفوف.
const pool = require("./db");
app.get("/api/products", async (req, res, next) => {
try {
const [rows] = await pool.query("SELECT id, name, price, stock FROM products ORDER BY name");
res.json(rows);
} catch (err) {
next(err);
}
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 33: العناصر النائبة ضد حقن SQL
لا تدمج مدخلات المستخدم في نص SQL أبداً.
// Unsafe: never build SQL by joining user input
// const [rows] = await pool.query("SELECT * FROM products WHERE name = '" + name + "'");
// Safe: the driver escapes the value
const [rows] = await pool.query("SELECT * FROM products WHERE name = ?", [name]);
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 34: إدراج وإعادة المعرّف الجديد
أنشئ صفاً وأبلغ العميل بمعرّفه.
app.post("/api/products", async (req, res, next) => {
try {
const { name, price, stock = 0 } = req.body;
const [result] = await pool.execute(
"INSERT INTO products (name, price, stock) VALUES (?, ?, ?)",
[name, price, stock]
);
res.status(201).json({ id: result.insertId, name, price, stock });
} catch (err) {
next(err);
}
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 35: تحديث مع تحقق
أعد 404 عندما لا يطابق أي صف.
app.put("/api/products/:id", async (req, res, next) => {
try {
const { price, stock } = req.body;
const [result] = await pool.execute(
"UPDATE products SET price = ?, stock = ? WHERE id = ?",
[price, stock, req.params.id]
);
if (result.affectedRows === 0) return res.status(404).json({ error: "Not found" });
res.json({ updated: true });
} catch (err) {
next(err);
}
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 36: حذف مع تحقق
أعد 204 أو 404 حسب النتيجة.
app.delete("/api/products/:id", async (req, res, next) => {
try {
const [result] = await pool.execute("DELETE FROM products WHERE id = ?", [req.params.id]);
res.status(result.affectedRows ? 204 : 404).end();
} catch (err) {
next(err);
}
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 37: المعاملة Transaction
احجز المخزون وأنشئ الطلب معاً، أو لا تفعل شيئاً.
async function placeOrder(productId, quantity) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const [stock] = await conn.execute("SELECT stock FROM products WHERE id = ? FOR UPDATE", [productId]);
if (stock[0].stock < quantity) throw new Error("not enough stock");
await conn.execute("UPDATE products SET stock = stock - ? WHERE id = ?", [quantity, productId]);
await conn.execute("INSERT INTO orders (product_id, quantity) VALUES (?, ?)", [productId, quantity]);
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release();
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 38: الترقيم إلى صفحات
أعد النتائج صفحةً صفحةً بدلاً من الجدول كاملاً.
app.get("/api/products", async (req, res, next) => {
const page = Math.max(1, Number(req.query.page) || 1);
const size = Math.min(50, Number(req.query.size) || 20);
try {
const [rows] = await pool.execute(
"SELECT id, name, price FROM products ORDER BY id LIMIT ? OFFSET ?",
[size, (page - 1) * size]
);
res.json({ page, size, items: rows });
} catch (err) {
next(err);
}
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 6: التحقق من الصحة ومعالجة الأخطاء
لا ينبغي أن تثق الواجهة البرمجية بالمتصفح أبداً. تحقّق من كل مدخل، وأعد رسائل واضحة للأخطاء، وحوّل أخطاء قاعدة البيانات إلى استجابات تستطيع الواجهة عرضها دون كشف تفاصيل داخلية.
التحقق على الخادم
تحسّن الفحوصات في Angular تجربة الاستخدام، لكن الخادم يجب أن يتحقق مرة أخرى لأن أي شخص يستطيع إرسال طلب مباشر. تصف مكتبة المخططات Schema الحقول مرة واحدة وتُبلغ عن كل المشكلات في استجابة واحدة.
شكل موحّد للأخطاء
استخدم معالج أخطاء مركزياً يعيد الشكل نفسه من JSON لكل فشل. حوّل أخطاء قاعدة البيانات المعروفة مثل المفاتيح المكررة إلى 409، وسجّل الخطأ كاملاً على الخادم، لكن أرسل للعميل رسالة آمنة فقط.
أخطاء المسارات غير المتزامنة
لا يلتقط Express 4 تلقائياً الوعود المرفوضة من معالجات المسارات. لذا غلّف المسارات غير المتزامنة حتى تصل الأخطاء إلى معالج الأخطاء، أو استخدم Express 5 الذي يتولى ذلك بنفسه.
مثال 39: التحقق من المدخلات
افحص الحقول قبل وصولها إلى قاعدة البيانات.
function validateProduct(body) {
const errors = [];
if (typeof body.name !== "string" || body.name.trim().length < 2) errors.push("name must be at least 2 characters");
if (typeof body.price !== "number" || body.price < 0) errors.push("price must be a number >= 0");
return errors;
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 40: إعادة 400 لمدخلات خاطئة
اشرح كل خطأ تحقق للعميل.
app.post("/api/products", async (req, res, next) => {
const errors = validateProduct(req.body);
if (errors.length) return res.status(400).json({ errors });
// ...insert as in example 34
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 41: التحقق بمخطط schema
صف البيانات مرة واحدة وأعد استخدام القواعد.
const { z } = require("zod");
const productSchema = z.object({
name: z.string().min(2).max(120),
price: z.number().nonnegative(),
stock: z.number().int().nonnegative().default(0),
});
const parsed = productSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ errors: parsed.error.issues });
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 42: تحويل أخطاء قاعدة البيانات إلى استجابات
اربط التكرار والمراجع المفقودة برموز واضحة.
app.use((err, req, res, next) => {
if (err.code === "ER_DUP_ENTRY") return res.status(409).json({ error: "Already exists" });
if (err.code === "ER_NO_REFERENCED_ROW_2") return res.status(422).json({ error: "Related item not found" });
next(err);
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 43: صنف خطأ مخصص
احمل حالة HTTP مع رسالة الخطأ.
class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
// usage
throw new HttpError(404, "Product not found");
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 44: غلاف المسارات async
مرر أخطاء المسارات غير المتزامنة إلى المعالج.
const asyncRoute = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
app.get("/api/products/:id", asyncRoute(async (req, res) => {
const [rows] = await pool.execute("SELECT * FROM products WHERE id = ?", [req.params.id]);
if (!rows.length) throw new HttpError(404, "Product not found");
res.json(rows[0]);
}));
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 45: CORS لخادم تطوير Angular
اسمح للمتصفح باستدعاء الواجهة من منفذ آخر.
const cors = require("cors");
app.use(cors({ origin: "http://localhost:4200" }));
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 46: ترويسات الأمان
اضبط ترويسات HTTP آمنة باستخدام helmet.
const helmet = require("helmet");
app.use(helmet());
app.disable("x-powered-by");
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 7: المصادقة والتفويض
تثبت المصادقة هوية المستخدم، ويحدد التفويض ما يُسمح له بفعله. خزّن كلمات المرور كتجزئات Hashes، وأصدر رموزاً قصيرة الصلاحية، وتحقق من الدور في كل مسار محمي.
كلمات المرور المجزّأة
يحوّل bcrypt كلمة المرور إلى تجزئة أحادية الاتجاه مع Salt مدمج ومعامل تكلفة. خزّن التجزئة فقط، وقارن عند تسجيل الدخول بـ bcrypt.compare، ولا تسجّل كلمات المرور أبداً ولا تعيدها في الاستجابات.
الرموز ومدة صلاحيتها
يحمل JSON Web Token معرّف المستخدم ودوره، ويُوقَّع بسر. اجعل مدته قصيرة، مثل ساعة واحدة، واستخدم رمز تحديث قابلاً للإلغاء للجلسات الأطول. خزّن رمز التحديث كتجزئة في قاعدة البيانات.
الأدوار وحماية المسارات
تحقق من الرمز في الوسيطة، ثم تحقق من الدور للإجراءات المخصصة للمدير. تذكّر أن الواجهة الأمامية تستطيع إخفاء زر، لكن الخادم وحده يستطيع فرض القاعدة.
مثال 47: تجزئة كلمات المرور
خزّن تجزئة bcrypt، لا كلمة المرور نفسها.
const bcrypt = require("bcrypt");
async function hashPassword(plain) {
return bcrypt.hash(plain, 12);
}
async function checkPassword(plain, hash) {
return bcrypt.compare(plain, hash);
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 48: جدول المستخدمين
خزّن بريداً إلكترونياً فريداً وتجزئة كلمة المرور والدور.
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(190) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
role ENUM('customer', 'admin') NOT NULL DEFAULT 'customer',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 49: تسجيل مستخدم
أنشئ حساباً بكلمة مرور مجزّأة.
app.post("/api/auth/register", asyncRoute(async (req, res) => {
const { email, password } = req.body;
const hash = await hashPassword(password);
const [result] = await pool.execute(
"INSERT INTO users (email, password_hash) VALUES (?, ?)",
[email.toLowerCase(), hash]
);
res.status(201).json({ id: result.insertId, email });
})));
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 50: تسجيل الدخول وإصدار رمز
أعد JWT موقّعاً بعد كلمة مرور صحيحة.
const jwt = require("jsonwebtoken");
app.post("/api/auth/login", asyncRoute(async (req, res) => {
const [rows] = await pool.execute("SELECT id, password_hash, role FROM users WHERE email = ?", [req.body.email]);
if (!rows.length || !(await checkPassword(req.body.password, rows[0].password_hash))) {
throw new HttpError(401, "Wrong email or password");
}
const token = jwt.sign({ sub: rows[0].id, role: rows[0].role }, process.env.JWT_SECRET, { expiresIn: "1h" });
res.json({ token });
})));
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 51: التحقق من JWT في الوسيط
اقرأ رمز Bearer وارفض الرموز غير الصالحة أو المنتهية.
const jwt = require("jsonwebtoken");
function requireAuth(req, res, next) {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: "Missing token" });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: "Invalid or expired token" });
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 52: السماح لأدوار محددة فقط
أعد 403 عندما لا يملك المستخدم الدور المطلوب.
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 53: حماية المسارات
اشترط تسجيل الدخول للطلبات، واحصر الحذف بالمدير.
app.get("/api/orders", requireAuth, asyncRoute(async (req, res) => {
const [rows] = await pool.execute("SELECT id, product_id, quantity FROM orders WHERE user_id = ?", [req.user.sub]);
res.json(rows);
})));
app.delete("/api/products/:id", requireAuth, requireRole("admin"), asyncRoute(async (req, res) => {
await pool.execute("DELETE FROM products WHERE id = ?", [req.params.id]);
res.status(204).end();
}));
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 54: تخزين رموز التحديث
احفظ تجزئة كل رمز تحديث حتى يمكن إبطاله.
CREATE TABLE refresh_tokens (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
token_hash CHAR(64) NOT NULL,
expires_at DATETIME NOT NULL,
revoked TINYINT(1) NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 8: مكونات Angular والقوالب
يبني Angular الواجهة من المكونات. يحتوي كل مكون على صنف فيه المنطق، وقالب فيه الترميز، وأنماط. المكونات المستقلة وتدفق التحكم والربط هي الطريقة الحديثة للكتابة.
المكونات المستقلة
يسرد المكون المستقل استيراداته بنفسه ولا يحتاج إلى NgModule. أنشئه بـ ng generate component، واستخدم محدد الاسم في قالب آخر لعرضه.
الربط
يعرض الاستيفاء Interpolation القيم بين أقواس مزدوجة. يضبط ربط الخصائص خاصية DOM بين أقواس مربعة، ويستدعي ربط الأحداث دالة بين أقواس دائرية. الربط ثنائي الاتجاه ممكن، لكن تدفق البيانات في اتجاه واحد أسهل في التصحيح.
تدفق التحكم
استخدم @if لعرض المحتوى بشرط، وَ@for مع تعبير track لتكرار العناصر، وَ@switch للاختيار بين عدة عروض. التتبع بالمعرّف يُبقي الصفحة فعّالة عند تغيّر القائمة.
مثال 55: مكوّن Angular مستقل
مكوّن لا يحتاج إلى NgModule.
import { Component } from "@angular/core";
@Component({
selector: "app-hello",
standalone: true,
template: `<h1>Welcome to the shop</h1>`,
})
export class HelloComponent {}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 56: ربط البيانات في القالب
اعرض القيم واربط الخصائص وعالج النقرات.
<h2>{{ product.name }}</h2>
<p>Price: {{ product.price | currency:'THB' }}</p>
<button [disabled]="product.stock === 0" (click)="addToCart(product)">Add to cart</button>
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 57: التحكم في التدفق بـ @if وبـ @for
اعرض القوائم وحالات الفراغ داخل القالب.
@if (products.length > 0) {
<ul>
@for (p of products; track p.id) {
<li>{{ p.name }} ({{ p.stock }} left)</li>
}
</ul>
} @else {
<p>No products yet.</p>
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 58: المدخلات والمخرجات
مرر البيانات إلى مكوّن فرعي وأرسل الأحداث إلى الأعلى.
import { Component, Input, Output, EventEmitter } from "@angular/core";
@Component({ selector: "app-product-card", standalone: true, template: "" })
export class ProductCardComponent {
@Input({ required: true }) product!: { id: number; name: string; price: number };
@Output() added = new EventEmitter<number>();
add() {
this.added.emit(this.product.id);
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 59: تشغيل تطبيق مستقل
وفّر الموجّه وHttpClient عند بدء التطبيق.
import { bootstrapApplication } from "@angular/platform-browser";
import { provideRouter } from "@angular/router";
import { provideHttpClient } from "@angular/common/http";
import { AppComponent } from "./app/app.component";
import { routes } from "./app/app.routes";
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes), provideHttpClient()],
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 60: الإنشاء والتشغيل بواسطة CLI
أنشئ المكوّنات والخدمات ثم شغّل التطبيق.
cd web
ng generate component products/product-list
ng generate service products/product
ng serve --open
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 9: الخدمات وHTTP والتوجيه
تحتفظ الخدمات بالمنطق وتتواصل مع الواجهة البرمجية، فتبقى المكونات صغيرة. يربط الموجّه الروابط بالمكونات، وتحمي حارسات المسارات الصفحات، ويُبقي التحميل الكسول حجم التنزيل الأول صغيراً.
الخدمات القابلة للحقن وHttpClient
علّم الخدمة بـ providedIn root حتى ينشئ Angular نسخة واحدة للتطبيق. احقن HttpClient وأعد Observable مُحدَّد النوع بواجهة الاستجابة، فيتحقق المترجم من الحقول التي تستخدمها.
المسارات والمعاملات
عرّف المسارات كمصفوفة من أزواج المسار والمكوّن. اقرأ المعاملات من ActivatedRoute، وأضف في النهاية مساراً عاماً يعرض صفحة “غير موجود”.
الحارسات والتحميل الكسول
تعيد حارسة CanActivate القيمة true أو تحويلاً. تستخدم المسارات الكسولة loadComponent مع استيراد ديناميكي، فلا تُحمَّل شيفرة الإدارة إلا عندما يفتحها المستخدم.
مثال 61: خدمة قابلة للحقن
شارك الحالة بين المكوّنات بنسخة واحدة.
import { Injectable } from "@angular/core";
@Injectable({ providedIn: "root" })
export class CartService {
private items: { id: number; qty: number }[] = [];
add(id: number) {
const found = this.items.find((i) => i.id === id);
if (found) found.qty++;
else this.items.push({ id, qty: 1 });
}
count() {
return this.items.reduce((sum, i) => sum + i.qty, 0);
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 62: استدعاء الواجهة بـ HttpClient
طلبات مُنمَّطة تعيد Observable.
import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
export interface Product { id: number; name: string; price: number; stock: number; }
@Injectable({ providedIn: "root" })
export class ProductService {
private http = inject(HttpClient);
list(): Observable<Product[]> {
return this.http.get<Product[]>("/api/products");
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 63: تعريف المسارات
اربط الروابط بالمكوّنات مع صفحة احتياطية.
import { Routes } from "@angular/router";
export const routes: Routes = [
{ path: "", pathMatch: "full", redirectTo: "products" },
{ path: "products", component: ProductListComponent },
{ path: "products/:id", component: ProductDetailComponent },
{ path: "**", component: NotFoundComponent },
];
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 64: حارس المسار route guard
وجّه المستخدم غير المسجّل إلى صفحة الدخول.
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { AuthService } from "./auth.service";
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.isLoggedIn() || inject(Router).createUrlTree(["/login"]);
};
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 65: قراءة معامل المسار
حمّل منتجاً واحداً من الرابط.
import { Component, inject } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
@Component({ selector: "app-product-detail", standalone: true, template: "" })
export class ProductDetailComponent {
private route = inject(ActivatedRoute);
readonly id = Number(this.route.snapshot.paramMap.get("id"));
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 66: التحميل الكسول لميزة
حمّل شيفرة الإدارة عند الحاجة فقط.
export const routes: Routes = [
{
path: "admin",
canActivate: [authGuard],
loadComponent: () => import("./admin/admin.component").then((m) => m.AdminComponent),
},
];
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 10: النماذج والتحقق
تجمع النماذج البيانات التي تخزّنها الواجهة البرمجية. تحتفظ النماذج التفاعلية Reactive Forms بالنموذج في TypeScript، ما يجعل التحقق والاختبار مباشرين ويُبقي القالب بسيطاً.
النماذج التفاعلية
ابنِ النموذج بـ FormBuilder، وصف كل حقل بقيمته الافتراضية ومدققاته، واربط القالب بـ formGroup وformControlName. قيمة النموذج مُحدَّدة النوع، لذلك يتحقق المترجم من شيفرة الإرسال.
المدققات المخصصة وغير المتزامنة
المدقق دالة تعيد null عندما تكون القيمة صحيحة، أو كائناً يصف الخطأ. اكتب مدققاً للقواعد التي لا تغطيها المدققات الجاهزة، مثل القيم التي تحتوي على مسافات فقط.
عرض الأخطاء وإرسال البيانات
اعرض الرسائل بعد أن يلمس المستخدم الحقل فقط، حتى لا يبدأ النموذج في حالة خطأ. عندما يعيد الخادم أخطاء التحقق، اعرضها بجوار النموذج بدلاً من تنبيه عام.
مثال 67: نموذج تفاعلي reactive form
ابنِ النموذج في TypeScript مع المدققات.
import { Component, inject } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
@Component({ selector: "app-product-form", standalone: true, imports: [ReactiveFormsModule], templateUrl: "./product-form.html" })
export class ProductFormComponent {
private fb = inject(FormBuilder);
form = this.fb.nonNullable.group({
name: ["", [Validators.required, Validators.minLength(2)]],
price: [0, [Validators.required, Validators.min(0)]],
stock: [0, [Validators.min(0)]],
});
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 68: قالب النموذج
اربط حقول الإدخال بنموذج البيانات.
<form [formGroup]="form" (ngSubmit)="save()">
<label>Name <input formControlName="name"></label>
<label>Price <input type="number" formControlName="price"></label>
<label>Stock <input type="number" formControlName="stock"></label>
<button type="submit" [disabled]="form.invalid">Save</button>
</form>
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 69: مدقق مخصص
ارفض القيم التي تحتوي على مسافات فقط.
import { AbstractControl, ValidationErrors } from "@angular/forms";
export function noSpacesOnly(control: AbstractControl): ValidationErrors | null {
const value = String(control.value ?? "");
return value.trim().length === 0 && value.length > 0 ? { spacesOnly: true } : null;
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 70: عرض رسائل التحقق
اعرض الأخطاء بعد أن يلمس المستخدم الحقل.
@if (form.controls.name.touched && form.controls.name.invalid) {
<p class="error">
@if (form.controls.name.errors?.['required']) { Name is required. }
@if (form.controls.name.errors?.['minlength']) { Name must be at least 2 characters. }
</p>
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 71: إرسال النموذج إلى الواجهة
أرسل البيانات وانتقل إلى صفحة أخرى عند النجاح.
save() {
if (this.form.invalid) return;
this.http.post("/api/products", this.form.getRawValue()).subscribe({
next: () => this.router.navigate(["/products"]),
error: (err) => (this.serverError = err.error?.errors ?? ["Could not save"]),
});
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 72: عرض أخطاء الخادم
اعرض رسائل التحقق التي تعيدها الواجهة.
// After a 400 response, the API sends { errors: ["price must be a number >= 0"] }
serverError: string[] = [];
// in the template
// @for (e of serverError; track e) { <p class="error">{{ e }}</p> }
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 11: الحالة وObservable والمعترِضات
تتغير البيانات مع الزمن، ويجب تنسيق طلبات كثيرة. تصف Observable وSignals هذا التغيير، وتضيف المعترِضات سلوكاً مشتركاً مثل الرموز وتسجيل الأخطاء إلى كل طلب.
Observable والأنبوب async
تُصدر Observable قيماً عبر الزمن. يشترك الأنبوب async داخل القالب ويلغي الاشتراك تلقائياً، فيتجنب تسرب الذاكرة وشيفرة الاشتراك اليدوي.
Signals للحالة المحلية
يحمل Signal قيمة تُخطر القالب عند تغيّرها. تشتق computed قيماً من الـ Signals، ويُشغّل effect الشيفرة عند تغيّرها. تناسب الـ Signals الحالات البسيطة مثل العداد أو إجمالي السلة.
المعترِضات
يرى معترض HTTP كل طلب واستجابة. استخدم واحداً لإضافة ترويسة Authorization، وآخر لتسجيل الإخفاقات أو التوجيه إلى صفحة الدخول عندما تنتهي صلاحية الرمز.
مثال 73: Observable وasync pipe
اعرض القائمة دون اشتراكات يدوية.
@Component({
selector: "app-products",
standalone: true,
imports: [AsyncPipe],
template: `
@for (p of products$ | async; track p.id) {
<div>{{ p.name }}</div>
}`,
})
export class ProductsComponent {
products$ = inject(ProductService).list();
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 74: معترض يضيف الرمز
أرفق ترويسة Authorization بكل طلب.
import { HttpInterceptorFn } from "@angular/common/http";
import { inject } from "@angular/core";
import { AuthService } from "./auth.service";
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 75: معترض أخطاء عام
سجّل فشل الواجهة في مكان واحد.
import { HttpErrorResponse, HttpInterceptorFn } from "@angular/common/http";
import { catchError, throwError } from "rxjs";
export const errorInterceptor: HttpInterceptorFn = (req, next) =>
next(req).pipe(
catchError((err: HttpErrorResponse) => {
console.error("API error", err.status, req.url);
return throwError(() => err);
})
);
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 76: Signals للحالة المحلية
تتبع قيمة واشتق منها قيمة أخرى.
import { Component, computed, signal } from "@angular/core";
@Component({ selector: "app-counter", standalone: true, template: "" })
export class CounterComponent {
count = signal(0);
double = computed(() => this.count() * 2);
increment() {
this.count.update((n) => n + 1);
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 77: مخزن صغير بـ Signals
احتفظ بحالة السلة في خدمة واحدة.
import { Injectable, computed, signal } from "@angular/core";
@Injectable({ providedIn: "root" })
export class CartStore {
private readonly lines = signal<{ id: number; qty: number }[]>([]);
readonly totalQty = computed(() => this.lines().reduce((s, l) => s + l.qty, 0));
add(id: number) {
this.lines.update((ls) => [...ls, { id, qty: 1 }]);
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 78: إلغاء الاشتراك تلقائياً
أوقف التدفق عند تدمير المكوّن.
import { Component, DestroyRef, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
export class ClockComponent {
constructor() {
interval(1000).pipe(takeUntilDestroyed()).subscribe(() => this.tick());
}
tick() {}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 12: التكامل بين طبقات Full-Stack
يكمن كثير من الإخفاقات في ربط الأجزاء ببعضها. يحافظ الوكيل أثناء التطوير وملفات البيئة لكل نشر والأنواع المشتركة على تطابق الواجهة الأمامية مع الواجهة البرمجية.
وكيل التطوير
وجّه /api من خادم تطوير Angular إلى خادم Node.js، فيرى المتصفح أصلاً واحداً ولا تحتاج إلى إعداد CORS أثناء التطوير. أعد تشغيل ng serve بعد تغيير ملف الوكيل.
ملفات البيئة
تمنح ملفات البيئة في Angular كل بناء إعداداته الخاصة، مثل عنوان الواجهة البرمجية. أبقِ عنوان الإنتاج خارج المصدر إذا كان سرياً، واقرأه من إعدادات البناء بدلاً من ذلك.
الأنواع المشتركة
صف شكل Product مرة واحدة في مجلد مشترك واستوردها في الواجهة البرمجية والواجهة الأمامية معاً. فإذا أعيدت تسمية حقل في جهة، يظهر خطأ في المترجم في الجهة الأخرى بدلاً من خطأ وقت التشغيل.
مثال 79: توجيه طلبات الواجهة أثناء التطوير
مرر /api إلى خادم Node.js.
{
"/api": {
"target": "http://localhost:3000",
"secure": false,
"changeOrigin": true
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 80: ملفات البيئة
استخدم عنوان واجهة مختلفاً في الإنتاج.
// src/environments/environment.ts
export const environment = {
production: false,
apiBaseUrl: "/api",
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
apiBaseUrl: "https://api.shop.example.com",
};
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 81: بناء عنوان الواجهة
ادمج العنوان الأساسي والمسار في مكان واحد.
import { environment } from "../environments/environment";
export const api = (path: string) => `${environment.apiBaseUrl}${path}`;
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 82: تشغيل الواجهة والواجهة الأمامية معاً
شغّل الخادمين في طرفيتين.
# terminal 1
cd api && npm run dev
# terminal 2
cd web && ng serve --proxy-config proxy.conf.json
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 83: مشاركة الأنواع بين الواجهة الأمامية والخلفية
أبقِ شكل Product متطابقاً على الطرفين.
// shared/product.ts (used by both web and api through a path alias)
export interface Product {
id: number;
name: string;
price: number;
stock: number;
}
export type CreateProduct = Omit<Product, "id">;
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 84: فحص صحة الواجهة من Angular
اعرض ما إذا كان الخادم الخلفي يستجيب.
export class AppComponent {
private http = inject(HttpClient);
apiOk = signal<boolean | null>(null);
constructor() {
this.http.get<{ status: string }>("/api/health").subscribe({
next: (r) => this.apiOk.set(r.status === "ok"),
error: () => this.apiOk.set(false),
});
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 13: اختبار كل طبقة
تكشف الاختبارات الأخطاء قبل أن يلاحظها المستخدمون. اختبر الدوال النقية ومسارات الواجهة البرمجية ومكونات Angular وخدماته، وأخيراً النظام كله مع قاعدة بيانات منفصلة.
اختبارات الواجهة البرمجية
يرسل Supertest الطلبات إلى تطبيق Express دون فتح منفذ حقيقي، ويتحقق Jest أو node:test من النتيجة. اختبر مسار النجاح وحالة فشل واحدة على الأقل، مثل غياب الرمز أو سعر غير صالح.
اختبارات Angular
ينشئ TestBed المكوّن مع تبعياته. يجيب HttpTestingController على استدعاءات HTTP ببيانات وهمية، فيمكن اختبار المكوّن دون تشغيل الواجهة البرمجية.
التكامل والتغطية
شغّل اختبارات التكامل على قاعدة بيانات اختبار منفصلة تُعاد تهيئتها قبل كل تشغيل، حتى لا تغيّر الاختبارات البيانات الحقيقية. تُظهر التغطية الشيفرة التي لا يصل إليها أي اختبار، وهي نقطة بداية وليست ضماناً.
مثال 85: اختبار مسار Express
استدعِ المسار بـ Supertest وتحقق من الرد.
const request = require("supertest");
const app = require("../app");
test("GET /api/health returns ok", async () => {
const res = await request(app).get("/api/health");
expect(res.status).toBe(200);
expect(res.body.status).toBe("ok");
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 86: اختبار دالة نقية
استخدم مشغّل node:test المدمج.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { validateProduct } = require("../validate");
test("rejects a short name", () => {
assert.equal(validateProduct({ name: "A", price: 10 }).length, 1);
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 87: اختبار مكوّن
أنشئ المكوّن بـ TestBed.
import { TestBed } from "@angular/core/testing";
import { ProductListComponent } from "./product-list.component";
it("creates the component", () => {
TestBed.configureTestingModule({ imports: [ProductListComponent] });
const fixture = TestBed.createComponent(ProductListComponent);
expect(fixture.componentInstance).toBeTruthy();
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 88: محاكاة HTTP بـ HttpTestingController
أجب الطلبات دون خادم حقيقي.
import { TestBed } from "@angular/core/testing";
import { provideHttpClient } from "@angular/common/http";
import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing";
import { ProductService } from "./product.service";
it("loads products", () => {
TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting()] });
const service = TestBed.inject(ProductService);
const http = TestBed.inject(HttpTestingController);
service.list().subscribe((items) => expect(items.length).toBe(1));
http.expectOne("/api/products").flush([{ id: 1, name: "Mouse", price: 350, stock: 3 }]);
http.verify();
});
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 89: اختبار تكامل مع قاعدة بيانات تجريبية
شغّل الواجهة مع قاعدة بيانات منفصلة.
# create a separate test database
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS shop_test"
DB_NAME=shop_test npm test -- --runInBand
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 90: تقارير التغطية
قِس مقدار الشيفرة التي تصل إليها الاختبارات.
cd api && npx jest --coverage
cd ../web && ng test --code-coverage --watch=false
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 14: Docker والنشر
تحزم الحاويات كل جزء مع تبعياته الدقيقة. يشغّل Docker Compose قاعدة البيانات والواجهة البرمجية وخادم الويب معاً، ويمكن للصور نفسها أن تعمل على حاسوب محمول أو خادم اختبار أو خدمة سحابية.
صور الواجهة البرمجية وتطبيق الويب
استخدم بناءات متعددة المراحل: ثبّت وابنِ في مرحلة واحدة، ثم انسخ إلى الصورة النهائية ما تحتاجه فقط. تعمل صورة الواجهة البرمجية بمستخدم غير root، وتحتوي صورة Angular على الملفات المبنية التي يقدمها Nginx فقط.
Compose وبدء قاعدة البيانات
يبدأ Compose تشغيل MySQL أولاً، ثم ينتظر نجاح فحص الصحة قبل تشغيل الواجهة البرمجية. ضع سكربتات المخطط في مجلد init، فيشغّلها MySQL فقط عندما يكون حجم البيانات فارغاً.
الوكيل العكسي
يقدم Nginx ملفات Angular، ويمرّر /api إلى حاوية الواجهة البرمجية، ويطبّق قاعدة try_files التي تُبقي مسارات العميل صالحة عند تحديث الصفحة.
مثال 91: Dockerfile للواجهة
صورة إنتاج صغيرة تعمل بمستخدم غير root.
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 92: Dockerfile لتطبيق Angular
ابنِ التطبيق وقدّمه عبر Nginx.
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx ng build --configuration production
FROM nginx:1.27-alpine
COPY --from=build /app/dist/web/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 93: Compose مع MySQL والواجهة والويب
شغّل المجموعة كاملة بأمر واحد.
services:
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: shop
MYSQL_USER: shop
MYSQL_PASSWORD: <strong-password>
MYSQL_ROOT_PASSWORD: <root-password>
volumes:
- db-data:/var/lib/mysql
- ./db/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 20
api:
build: ./api
env_file: ./api/.env
depends_on:
db:
condition: service_healthy
web:
build: ./web
ports:
- "80:80"
depends_on:
- api
volumes:
db-data:
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 94: تشغيل المخطط عند أول بدء
أنشئ الجداول تلقائياً في حجم جديد.
# db/init/01-schema.sql is run once when the data volume is empty
services:
db:
volumes:
- ./db/init:/docker-entrypoint-initdb.d:ro
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 15: عمليات الإنتاج
لا يكتمل الإصدار لمجرد أن يعمل مرة واحدة. يحتاج الإنتاج إلى إعدادات آمنة، وسجلات منظمة، ونسخ احتياطية، وخط تكامل مستمر، وقائمة فحص توقف الإصدارات السيئة.
إعدادات آمنة
ارفض البدء عندما يكون مفتاح التوقيع مفقوداً، وثق برأس الوكيل فقط عندما يكون وكيل أمامك، وعطّل ترويسات الإطار التي تكشف برنامج الخادم.
السجلات والنسخ الاحتياطية
اكتب سجلات JSON منظمة تتضمن معرّف الطلب، حتى تُتبع المشكلة الواحدة عبر النظام كله. انسخ قاعدة البيانات احتياطياً بـ mysqldump مع --single-transaction، واختبر الاستعادة على جهاز آخر.
خط التكامل وقائمة فحص الإصدار
يشغّل سير عمل CI اختبارات الواجهة البرمجية واختبارات Angular وبناءات الإنتاج عند كل دفعة. يتحقق سكربت الإصدار من نقطة الصحة والحاويات قبل الإعلان عن النسخة الجديدة.
مثال 95: إعدادات الإنتاج في الواجهة
ارفض البدء دون مفتاح سري.
const isProd = process.env.NODE_ENV === "production";
if (isProd && !process.env.JWT_SECRET) {
throw new Error("JWT_SECRET must be set in production");
}
app.set("trust proxy", 1);
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 96: وكيل عكسي بـ Nginx
قدّم التطبيق ومرّر /api إلى الواجهة.
server {
listen 80;
root /usr/share/nginx/html;
location /api/ {
proxy_pass http://api:3000;
proxy_set_header Host $host;
}
location / {
try_files $uri /index.html;
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 97: سجلات منظمة بـ pino
اكتب سجلات JSON يمكن للأدوات البحث فيها.
const pino = require("pino");
const logger = pino({ level: process.env.LOG_LEVEL || "info" });
app.use(require("pino-http")({ logger }));
logger.info({ port: 3000 }, "API started");
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 98: نسخ احتياطي لقاعدة MySQL
أنشئ نسخة متسقة وضغطها.
mysqldump -h db -u shop -p shop --single-transaction > backup-$(date +%F).sql
gzip backup-$(date +%F).sql
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 99: سير عمل CI على GitHub Actions
اختبر وابنِ عند كل دفع إلى main.
name: ci
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci --prefix api
- run: npm test --prefix api
- run: npm ci --prefix web
- run: npx ng test --watch=false --prefix web
- run: npx ng build --configuration production --prefix web
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 100: سكربت قائمة فحص الإصدار
أوقف الإصدار إذا فشل أي فحص.
#!/usr/bin/env bash
set -euo pipefail
curl -fsS https://shop.example.com/api/health | grep -q '"status":"ok"'
docker compose ps --format json | grep -q '"Health":"healthy"'
gzip -t backup-latest.sql.gz
echo "release checks passed"
*لم يُشغَّل هذا المثال هنا لأن MySQL وAngular CLI لم يكونا مثبتين على الحاسوب الذي أُعدّت به هذه المقالة.*
المختبر: بناء متجر صغير واختباره وإصداره
اجمع المقالة في مشروع واحد: واجهة برمجية فيها تسجيل الدخول ومسارات المنتجات، ومخطط MySQL مع طبقة استعلامات آمنة، وصفحة منتجات في Angular مع نموذج تفاعلي، واختبارات لكل طبقة، وحزمة Docker Compose.
1. أنشئ مجلدي api وweb، وثبّت Express وmysql2 وdotenv وbcrypt في الواجهة البرمجية، وأنشئ تطبيق Angular مع التوجيه.
2. أنشئ جدولي products وusers في MySQL بمستخدم ذي صلاحيات محدودة، ثم أضف بعض المنتجات.
3. اكتب مسارات المنتجات مع المعاملات النائبة والتحقق ومعالج الأخطاء المركزي، واختبرها بـ curl أو Supertest.
4. أضف التسجيل والدخول بكلمات مرور مجزّأة و JWT، ثم احمِ مسار الطلبات بالوسيطة.
5. أنشئ قائمة منتجات Angular بـ HttpClient ومساراً لتفاصيل المنتج ونموذجاً تفاعلياً لإضافة منتج.
6. أضف ملف الوكيل، وشغّل الواجهة البرمجية وتطبيق الويب معاً، ثم اختبر التدفق كاملاً في المتصفح.
7. اكتب اختبار API واختبار مكوّن واحدين وشغّلهما، ثم شغّل النظام كله بـ docker compose up وشغّل قائمة الفحص قبل الإصدار من المثال 100.
الخطوات التالية
بعد المختبر، اقرأ الوثائق الرسمية لـ Angular وExpress وMySQL، وأضف الترقيم والبحث إلى المتجر، وابنِ ميزة ثانية من البداية إلى النهاية دون نسخ الشيفرة. احتفظ بالأمثلة في مستودع وأعد تشغيل الاختبارات بعد كل ترقية للحزم، لأن أطر العمل تتغير بين الإصدارات الرئيسية.
الخلاصة
يربط التطبيق متكامل الطبقات ثلاثة أجزاء يجب أن تتفق مع بعضها. تعلمت إعداد المشروع والأدوات، وكتابة شيفرة Node.js وExpress، وتصميم جداول MySQL والاستعلامات الآمنة، وربط الواجهة البرمجية بالمجمّعات والمعاملات، والتحقق من المدخلات ومعالجة الأخطاء، وحماية المسارات بكلمات مرور مجزّأة ورموز، وبناء مكونات Angular والخدمات والمسارات والنماذج والمعترِضات، وربط الواجهة الأمامية بالواجهة البرمجية، واختبار كل طبقة، وتجميع النظام في Docker، والتجهيز للإنتاج.