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

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

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

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

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

كل منشورات العضو ابراهيم الخضور

  1. نلقي نظرةً في هذا المقال على الطرق المتبعة في نقل التغييرات التي تجري أثناء جلسة العمل إلى قاعدة البيانات العلاقية وتخزينها، إضافةً إلى ربط الجداول ببعضها من خلال المفاتيح الخارجية foreign keys. ملفات التهجير Migrations لنتابع في توسعة تطبيق الملاحظات الذي عملنا عليه في كل من مقال العمل مع قواعد بيانات علاقية باستخدام Sequelize والمقال السابق، إذ سننجز آليةً تمكّن بعض المستخدمين الذي يمتلكون ميزةً إدارية admin status من إيقاف نشاط مستخدمين آخرين ومنعهم من تسجيل الدخول أو إنشاء ملاحظات جديدة. نحتاج لإنجاز الأمر إلى حقل يأخذ قيمًا منطقيةً في جدول قاعدة البيانات يشير إلى المستخدمين ذوي الامتيازات الإدارية وأخرى تدل على حالة المستخدم إن كان نشاطه معلقًا أم لا. يمكننا العمل وفق الآلية السابقة بتعديل النموذج الذي يعرّف الجدول والاعتماد على مكتبة Sequelize في مزامنة التغييرات مع قاعدة البيانات من خلال أسطر الشيفرة التالية في الملف "models/index.js": const Note = require('./note') const User = require('./user') Note.belongsTo(User) User.hasMany(Note) Note.sync({ alter: true }) User.sync({ alter: true }) module.exports = { Note, User } ولكن يُعد هذا الأسلوب (تغيير النموذج كلما أردنا توسعة التطبيق) غير منطقي على المدى الطويل، لهذا سنزيل الأسطر التي تدعم مزامنة التغييرات ونستخدم أسلوبًا أكثر قوةً وهو ملفات التهجير migrations التي تقدمها Sequelize وغيرها من المكتبات. ملف التهجير migration عمليًا هو ملف جافاسكربت JavaScript وحيد يصف التعديلات التي تطرأ على قاعدة البيانات، وينشأ عن تغير وحيد أو مجموعة تغيرات معًا. تحتفظ Sequelize بسجلات عن التهجيرات التي أجريت، أي التغيرات التي طرأت وجرى نقلها ومزامنتها مع قاعدة البيانات، إذ تبقى Sequelize مطلّعةً على التغيرات التي طرأت على مخطط قاعدة البيانات ولم تُطبّق بعد عند إنشاء تهجير جديد، ويمكن بهذه الطريقة التحكم بطريقة تطبيق التغييرات من خلال شيفرة البرنامج المخزنة في منظومة التحكم بالإصدار version control. لننشئ أولًا ملف تهجير يهيئ قاعدة البيانات. إليك شيفرة هذا الملف: const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.createTable('notes', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE }, }) await queryInterface.createTable('users', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, username: { type: DataTypes.STRING, unique: true, allowNull: false }, name: { type: DataTypes.STRING, allowNull: false }, }) await queryInterface.addColumn('notes', 'user_id', { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }) }, down: async ({ context: queryInterface }) => { await queryInterface.dropTable('notes') await queryInterface.dropTable('users') }, } يعرِّف الملف الدالتين up و down؛ إذ تحدّد الأولى كيفية تعديل قاعدة البيانات عندما يجري تهجير؛ وتحدد الثانية آلية التراجع إن كان هناك مبررٌ لذلك. يتضمن ملف التهجير ثلاثة خيارات؛ إذ يُنشئ الخيار الأول الجدول notes؛ بينما ينشئ الثاني الجدول users؛ ويضيف الخيار الثالث مفتاحًا خارجيًا إلى الجدول notes يشير إلى مُنشئ الملاحظة، وتُحدَّد التغيرات في المخطط باستدعاء توابع الكائن queryInterface. و على نقيض النماذج، من الضروري أن تتذكر دائمًا كتابة أسماء الأعمدة والجداول بأسلوب الأفعى snake case عند تعريف ملف تهجير: await queryInterface.addColumn('notes', 'user_id', { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }) أي ستكتب أسماء الجداول والأعمدة كما تظهر تمامًا في قاعدة البيانات، بينما تُكتب في النماذج بالأسلوب التقليدي للمكتبة Sequelize وذلك عبر استخدام أسلوب سنام الجمل camel Case. خزّن شيفرة ملف التهجير في ملف يحمل الاسم "migrations/20211209_00_initialize_notes_and_users.js". لا بُد أن تُسمى ملفات التهجير أبجديًا عند إنشائها كي تكون التغيرات القديمة قبل الجديدة، وقد تبدأ اسم الملف بتاريخ وتسلسل هذا الملف وهذه طريقةٌ جيدة. يمكننا تشغيل ملف التهجير انطلاقًا من سطر الأوامر باستخدام أداة سطر الأوامر الخاصة بالمكتبة Sequelize، لكننا قررنا تنفيذ التهجيرات يديويًا انطلاقًا من شيفرة البرنامج باستخدام المكتبة Umzug، لهذا سنثبت هذه المكتبة: npm install umzug لنعدّل الملف "util/db.js" الذي يتكفل بمعالجة الاتصال مع قاعدة البيانات: const Sequelize = require('sequelize') const { DATABASE_URL } = require('./config') const { Umzug, SequelizeStorage } = require('umzug') const sequelize = new Sequelize(DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); const runMigrations = async () => { const migrator = new Umzug({ migrations: { glob: 'migrations/*.js', }, storage: new SequelizeStorage({ sequelize, tableName: 'migrations' }), context: sequelize.getQueryInterface(), logger: console, }) const migrations = await migrator.up() console.log('Migrations up to date', { files: migrations.map((mig) => mig.name), }) } const connectToDatabase = async () => { try { await sequelize.authenticate() // highlight-start await runMigrations() // highlight-end console.log('connected to the database') } catch (err) { console.log('failed to connect to the database') console.log(err) return process.exit(1) } return null } module.exports = { connectToDatabase, sequelize } تُنفّذ الدالة runMigrations ملف التهجير في كل مرة يؤسِّس فيها التطبيق اتصالًا مع قاعدة بيانات عند تشغيله، كما تراقب مكتبة Sequelize التهجيرات المُكتملة، فإذا لم يكن هناك تهجيرات جديدة، فلن ينفع تشغيل الدالة runMigrations في أي شيء. لنبدأ كل شيء من جديد، ونزيل كل الجداول الموجودة من التطبيق: username => drop table notes; username => drop table users; username => \d Did not find any relations. عند تشغيل التطبيق، تظهر رسالةً على الطرفية تحيطك علمًا بوضع التهجيرات: INSERT INTO "migrations" ("name") VALUES ($1) RETURNING "name"; Migrations up to date { files: [ '20211209_00_initialize_notes_and_users.js' ] } database connected إذا أعدنا تشغيل التطبيق، سيُظهر السجل أن التهجير لم يتكرر، وسيبدو مخطط قاعدة بيانات التطبيق على النحو التالي: username=> \d List of relations Schema | Name | Type | Owner --------+--------------+----------+---------------- public | migrations | table | username public | notes | table | username public | notes_id_seq | sequence | username public | users | table | username public | users_id_seq | sequence | username وهكذا نرى أن Sequelize قد أنشأت جدولًا للتهجيرات يسمح بتتبع ما نُفِّذ منها، ويبدو هذا الجدول على النحو التالي: username=> select * from migrations; name ------------------------------------------- 20211209_00_initialize_notes_and_users.js دعونا نضيف الآن بعض المستخدمين إلى قاعدة البيانات، إضافةً إلى مجموعة من الملاحظات، وبعدها نكون جاهزين لتوسعة التطبيق. يمكنك الحصول على شيفرة التطبيق بوضعه الحالي من المستودع المخصص له على GitHub ضمن الفرع part13-6. مستخدم بصلاحيات مدير وتعطيل مستخدم آخر علينا بدايةً إضافة حقلين يقبلان قيمًا منطقية إلى الجدول users، هما: admin: ويحدد فيما لو كان المستخدم مديرًا أم لا. disabled: ويحدد فيما لو أُوقف نشاط هذا المستخدم أم لا. لننشئ أيضًا ملف تهجير يُعدّل قاعدة بيانات في الملف "migrations/20211209_01_admin_and_disabled_to_users.js": const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.addColumn('users', 'admin', { type: DataTypes.BOOLEAN, default: false }) await queryInterface.addColumn('users', 'disabled', { type: DataTypes.BOOLEAN, default: false }) }, down: async ({ context: queryInterface }) => { await queryInterface.removeColumn('users', 'admin') await queryInterface.removeColumn('users', 'disabled') }, } عدّل النموذج بما يتوافق مع الجدول "users": User.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, username: { type: DataTypes.STRING, unique: true, allowNull: false }, name: { type: DataTypes.STRING, allowNull: false }, admin: { type: DataTypes.BOOLEAN, defaultValue: false }, disabled: { type: DataTypes.BOOLEAN, defaultValue: false }, }, { sequelize, underscored: true, timestamps: false, modelName: 'user' }) يتغير المخطط كما نريد، عندما تُنفَّذ شيفرة ملف التهجير عند إعادة تشغيل التطبيق: username-> \d users Table "public.users" Column | Type | Collation | Nullable | Default ----------+------------------------+-----------+----------+----------------------------------- id | integer | | not null | nextval('users_id_seq'::regclass) username | character varying(255) | | not null | name | character varying(255) | | not null | admin | boolean | | | disabled | boolean | | | Indexes: "users_pkey" PRIMARY KEY, btree (id) "users_username_key" UNIQUE CONSTRAINT, btree (username) Referenced by: TABLE "notes" CONSTRAINT "notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) لنوسّع الآن المتحكمات على النحو التالي، بحيث نمنع المستخدم من تسجيل الدخول في حال كانت قيمة الحقل disabled هي true: loginRouter.post('/', async (request, response) => { const body = request.body const user = await User.findOne({ where: { username: body.username } }) const passwordCorrect = body.password === 'secret' if (!(user && passwordCorrect)) { return response.status(401).json({ error: 'invalid username or password' }) } if (user.disabled) { return response.status(401).json({ error: 'account disabled, please contact admin' }) } const userForToken = { username: user.username, id: user.id, } const token = jwt.sign(userForToken, process.env.SECRET) response .status(200) .send({ token, username: user.username, name: user.name }) }) دعونا نوقف نشاط المستخدم "jakousa" بالاستعانة بمعرّفه الفريد ID: username => update users set disabled=true where id=3; UPDATE 1 username => update users set admin=true where id=1; UPDATE 1 username => select * from users; id | username | name | admin | disabled ----+----------+------------------+-------+---------- 2 | lynx | Kalle Ilves | | 3 | jakousa | Jami Kousa | f | t 1 | mluukkai | Matti Luukkainen | t | تأكد من نجاح الأمر بفشل تسجيل دخوله: لننشئ تاليًا وجهةً تسمح للمدير بتغيير حالة حساب مستخدم آخر: const isAdmin = async (req, res, next) => { const user = await User.findByPk(req.decodedToken.id) if (!user.admin) { return res.status(401).json({ error: 'operation not allowed' }) } next() } router.put('/:username', tokenExtractor, isAdmin, async (req, res) => { const user = await User.findOne({ where: { username: req.params.username } }) if (user) { user.disabled = req.body.disabled await user.save() res.json(user) } else { res.status(404).end() } }) تُستخدم في هذه الشيفرة أداتان وسطيتان، تُدعى الأولى tokenExtractor وتماثل الأداة التي استخدمناها لإنشاء الوجهة route الخاصة بإنشاء ملاحظة، إذ تضع مفتاح الاستيثاق الذي فُكّ تشفيره في الحقل decodedToken من كائن الطلب؛ بينما تتحقق الأداة الثانية isAdmin فيما لوكان المستخدم ذا صلاحيات إدارية، فإن لم يكن كذلك يُضبط رمز حالة الطلب على القيمة 401 وتُعاد رسالة خطأ مناسبة. لاحظ كيف رُبطت الأداتين إلى الوجهة، إذ تُنفَّذ كلٌ منهما قبل تنفيذ معالج الوجهة الفعلي، ويمكنك ربط العدد الذي تريده من الأدوات الوسيطة إلى طلبٍ ما. تُنقل الأداة الوسطية tokenExtractor إلى الملف "util/middleware.js" كونها تُستخدم في عدّة مواضع: const jwt = require('jsonwebtoken') const { SECRET } = require('./config.js') const tokenExtractor = (req, res, next) => { const authorization = req.get('authorization') if (authorization && authorization.toLowerCase().startsWith('bearer ')) { try { req.decodedToken = jwt.verify(authorization.substring(7), SECRET) } catch{ return res.status(401).json({ error: 'token invalid' }) } } else { return res.status(401).json({ error: 'token missing' }) } next() } module.exports = { tokenExtractor } يمكن للمدير الآن السماح للمستخدم "jakousa" بمعاودة نشاطه عن طريق إرسال طلب من النوع PUT إلى الوجهة "api/users/jakousa/" إذ يأتي الطلب مع البيانات التالية: { "disabled": false } وكما رأينا في نهاية القسم 4، تحمل هذه الطريقة في إيقاف نشاط المستخدمين بعض المشاكل، إذ يجري التحقق من وضع المستخدم عند تسجيل الدخول، وإن امتلك المستخدم مفتاح استيثاق صحيح في لحظة إيقاف نشاطه، فقد يتمكن من متابعة استخدام مفتاحه لعدم وجود فترة زمنية لصلاحيته، ولن يجري التحقق من حالته عند إضافة ملاحظة جديدة. قبل أن نتابع، سنكتب سكربت npm يسمح لنا بالتراجع عن آخر تهجير، فقد لا ينجح كل شيء في المرة الأولى بالضرورة عند تطوير ملفات التهجير. لنعدّل الملف "util/db.js" على النحو التالي: const Sequelize = require('sequelize') const { DATABASE_URL } = require('./config') const { Umzug, SequelizeStorage } = require('umzug') const sequelize = new Sequelize(DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); const connectToDatabase = async () => { try { await sequelize.authenticate() await runMigrations() console.log('connected to the database') } catch (err) { console.log('failed to connect to the database') return process.exit(1) } return null } const migrationConf = { migrations: { glob: 'migrations/*.js', }, storage: new SequelizeStorage({ sequelize, tableName: 'migrations' }), context: sequelize.getQueryInterface(), logger: console, } const runMigrations = async () => { const migrator = new Umzug(migrationConf) const migrations = await migrator.up() console.log('Migrations up to date', { files: migrations.map((mig) => mig.name), }) } const rollbackMigration = async () => { await sequelize.authenticate() const migrator = new Umzug(migrationConf) await migrator.down() } module.exports = { connectToDatabase, sequelize, rollbackMigration } // highlight-line لننشئ الملف "util/rollback.js" الذي يتيح لسكربت npm تنفيذ دالة التراجع عن التهجير: const { rollbackMigration } = require('./db') rollbackMigration() وسيكون السكربت على النحو التالي: { "scripts": { "dev": "nodemon index.js", "migration:down": "node util/rollback.js" }, } وهكذا سنتمكن من التراجع عن آخر تهجير بتنفيذ الأمر npm run migration:down من خلال سطر الأوامر. تُنفّذ ملفات التهجير تلقائيًا عندما يُشغَّل البرنامج، لكن قد يكون من الأنسب في مرحلة التطوير تعطيل التنفيذ التلقائي للتهجير وتنفيذه يدويًا من خلال سطر الأوامر. يمكنك الحصول على شيفرة التطبيق بوضعه الحالي من المستودع المخصص له على GitHub ضمن الفرع part13-7. التمرينان 13.7 و 13.8 حاول إنجاز التمارين التالية: التمرين 13.7 احذف كل الجداول من قاعدة بيانات تطبيقك ثم أنشئ ملف تهجير ليهيئ قاعدة البيانات. أضف البصمتان الزمنيتان "created_at" و "updated_at" إلى كلا الجدولين، وتذكر أن عليك إضافتها في ملف التهجير بنفسك. ملاحظة: تأكد من إزالة التعليمتين ()User.sync و ()Blog.sync اللتين تزامنان مخطط النموذج من شيفرتك وإلا سيخفق التهجير. ملاحظة: إذا كان عليك حذف الجداول باستخدام سطر الأوامر، أي إذا لم تكن تنوي التراجع عن الحذف بالتراجع عن آخر عملية تهجير، فلا بُد حينها من حذف محتوى جدول التهجير migrations إذا أردت من برنامجك تنفيذ التهجير مجددًا. التمرين 13.18 وسّع تطبيقك (مستخدمًا ملف تهجير) لكي يكون للمدونة سمةً تدل على سنة كتابتها، أي حقلٌ بالاسم يأخذ قيمًا صحيحة تبدأ من 1991 ولا يزيد عن العام الحالي. تأكد من ظهور رسالة خطأ مناسبة إن أُدخلت قيمة غير صحيحة للسنة. علاقات متعدد-إلى-متعدد many-to-many بين الجداول سنتابع توسعة التطبيق كي يُضاف كل مستخدم إلى فريقٍ أو أكثر، وطالما يمكن لأي عدد من المستخدمين الانضمام إلى فريق وكذلك يمكن لأي مستخدم الانضمام إلى أي عدد من الفرقاء، فإننا أمام علاقة متعدد-إلى-متعدد many-to-many والتي تنُفّذ تقليديًا في قواعد البيانات من خلال جدول الاتصال connection table. لنكتب الآن الشيفرة التي يحتاجها الجدول teams إضافةً إلى جدول الاتصال. سيبدو ملف التهجير أولًا على النحو التالي: const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.createTable('teams', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.TEXT, allowNull: false, unique: true }, }) await queryInterface.createTable('memberships', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, user_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, team_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'teams', key: 'id' }, }, }) }, down: async ({ context: queryInterface }) => { await queryInterface.dropTable('teams') await queryInterface.dropTable('memberships') }, } تتضمن النماذج نفس شيفرة ملفات التهجير تقريبًا، وإليك شيفرة نموذج الجدول team في الملف "models/team.js": const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class Team extends Model {} Team.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.TEXT, allowNull: false, unique: true }, }, { sequelize, underscored: true, timestamps: false, modelName: 'team' }) module.exports = Team وهذه هي شيفرة نموذج جدول الاتصال الموجودة في الملف "models/membership.js": const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class Membership extends Model {} Membership.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, user_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, team_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'teams', key: 'id' }, }, }, { sequelize, underscored: true, timestamps: false, modelName: 'membership' }) module.exports = Membership منحنا جدول الاتصال اسمًا يصف طبيعة محتوياته membership، لكن قد لا تجد بالضرورة اسمًا يصف هذا الجدول، ولهذا يمكنك تسميته بأسماء الجداول التي يربطها تفصل بينها شرطة سفلية، مثل "user_teams" الذي يصلح أيضًا لحالتنا هذه. أجرينا إضافةً صغيرةً على الملف "models/index.js" لربط الفرقاء والمستخدمين على مستوى الشيفرة باستخدام التابع belongsToMany: const Note = require('./note') const User = require('./user') const Team = require('./team') const Membership = require('./membership') Note.belongsTo(User) User.hasMany(Note) User.belongsToMany(Team, { through: Membership }) Team.belongsToMany(User, { through: Membership }) module.exports = { Note, User, Team, Membership } لاحظ الفَرق بين ملف التهجير لجدول الاتصال ونموذجه عند تعريف مفتاح خارجي، إذ تُعرَّف الحقول باستخدام أسلوب الأفعى في ملف التهجير: await queryInterface.createTable('memberships', { // ... user_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, team_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'teams', key: 'id' }, } }) بينما تُعرَّف نفس الحقول في النموذج باستخدام أسلوب سنام الجمل: Membership.init({ // ... userId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, teamId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'teams', key: 'id' }, }, // ... }) لننشئ الآن بعض الفُرقاء وبعض الأعضاء باستخدام الطرفية: insert into teams (name) values ('toska'); insert into teams (name) values ('mosa climbers'); insert into memberships (user_id, team_id) values (1, 1); insert into memberships (user_id, team_id) values (1, 2); insert into memberships (user_id, team_id) values (2, 1); insert into memberships (user_id, team_id) values (3, 2); تُضاف معلومات أعضاء الفرقاء بعد ذلك إلى الوجهة التي تعيد كل المستخدمين: router.get('/', async (req, res) => { const users = await User.findAll({ include: [ { model: Note, attributes: { exclude: ['userId'] } }, { model: Team, attributes: ['name', 'id'], } ] }) res.json(users) }) لاحظ أن الاستعلام الذي طُبع على شاشة الطرفية يجمع ثلاثة جداول. هذا الحل جيدٌ فعلًا، لكن ما تزال هنالك ثغرة، إذ تأتي نتيجة الاستعلام مع جميع سمات الصف المطلوب من جدول الاتصال، علمًا أننا لا نحتاجها كلها. وبقراءة توثيق Sequelize جيدًا ستجد الحل: router.get('/', async (req, res) => { const users = await User.findAll({ include: [ { model: Note, attributes: { exclude: ['userId'] } }, { model: Team, attributes: ['name', 'id'], through: { attributes: [] } } ] }) res.json(users) }) يمكنك الحصول على شيفرة التطبيق بوضعه الحالي من المستودع المخصص له على GitHub ضمن الفرع part13-8. فكرة عن خاصيات كائن نموذج Sequelize تظهر خاصيات نموذجنا من خلال الأسطر التالية: User.hasMany(Note) Note.belongsTo(User) User.belongsToMany(Team, { through: Membership }) Team.belongsToMany(User, { through: Membership }) يمكّن ذلك Sequelize من إنشاء استعلامات تستخلص مثلًا كل ملاحظات المستخدمين أو كل أعضاء فريق، وبفضل تلك التعريفات نستطيع أيضًا الوصول مباشرةً إلى ملاحظات مستخدم مثلًا من خلال الشيفرة؛ ففي الشيفرة التالية مثلًا نحاول البحث عن مستخدم معرّفه المميز id=1، ثم نطبع الملاحظات المرتبطة به: const user = await User.findByPk(1, { include: { model: Note } }) user.notes.forEach(note => { console.log(note.content) }) يربط التعريف: User.hasMany(Note) الخاصية notes إلى الكائن user الذي يمنح وصولًا إلى الملاحظات التي أنشأها المستخدم، ويربط التعريف: User.belongsToMany(Team, { through: Membership }) الخاصية teams إلى الكائن user، الذي يمكن استخدامه في الشيفرة: const user = await User.findByPk(1, { include: { model: team } }) user.teams.forEach(team => { console.log(team.name) }) لنفترض أننا نريد إعادة كائن JSON من وجهة route تقود إلى مستخدم واحد يتضمن اسم المستخدم وعدد الملاحظات التي أنشأها. يمكننا تجربة الطريقة التالية: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { include: { model: Note } } ) if (user) { user.note_count = user.notes.length delete user.notes res.json(user) } else { res.status(404).end() } }) حاولنا إضافة الحقل noteCount في الكائن الذي أعادته Sequelize وإزالة الحقل notes منه، لكن ما يحدث أن هذه الطريقة لن تنفع لأن الكائن الذي تعيده Sequelize ليس كائنًا عاديًا تعمل فيه الحقول الإضافية كما نريد؛ ولذلك فإن الحل الأفضل لحالتنا هو إنشاء كائن جديد كليًا بناءً على البيانات المستخلصة من قاعدة البيانات: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { include: { model: Note } } ) if (user) { res.json({ username: user.username, name: user.name, note_count: user.notes.length }) } else { res.status(404).end() } }) نظرة ثانية إلى العلاقات متعدد-إلى-متعدد many-to-many سنختبر علاقة متعدد-إلى-متعدد أخرى في التطبيق؛ إذ ترتبط كل ملاحظة بالمستخدم الذي أنشأها من خلال مفتاح خارجي، ونريد الآن أن يدعم التطبيق ربط الملاحظة بمستخدمين آخرين، وربط المستخدم بعدد غير محدد من الملاحظات التي أنشأها آخرون، والفكرة هنا أن هذه الملاحظات هي تلك التي أشار المستخدم على أنها تهمّه. لننشئ جدول الاتصال user_notes في هذه الحالة، وسيكون ملف التهجير بسيطًا: const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.createTable('user_notes', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, user_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, note_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'notes', key: 'id' }, }, }) }, down: async ({ context: queryInterface }) => { await queryInterface.dropTable('user_notes') }, } كما لن تجد أفكارًا جديدةً أيضًا في شيفرة النموذج: const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class UserNotes extends Model {} UserNotes.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, userId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, noteId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'notes', key: 'id' }, }, }, { sequelize, underscored: true, timestamps: false, modelName: 'user_notes' }) module.exports = UserNotes يضم الملف "models/index.js" بعض التغيرات ليصبح على النحو التالي: const Note = require('./note') const User = require('./user') const Team = require('./team') const Membership = require('./membership') const UserNotes = require('./user_notes') Note.belongsTo(User) User.hasMany(Note) User.belongsToMany(Team, { through: Membership }) Team.belongsToMany(User, { through: Membership }) User.belongsToMany(Note, { through: UserNotes, as: 'marked_notes' }) Note.belongsToMany(User, { through: UserNotes, as: 'users_marked' }) module.exports = { Note, User, Team, Membership, UserNotes } يُستخدم التعريف belongsToMany مجددًا، إذ يربط الآن المستخدمين إلى الملاحظات عن طريق النموذج UserNotes المتعلق بجدول الاتصال، لكننا سنستخدم هذه المرة اسمًا بديلًا alias للسمات المُنشأة باستخدام الكلمة المحجوزة as، إذ سيتداخل overlap الاسم الافتراضي ("notes" المستخدم) مع معناه السابق وهو الملاحظات المُضافة من قِبل المستخدم. سنوسِّع الوجهة التي تقود إلى مستخدم وحيد لتعيد الفُرقاء التي ينتمي إليها المستخدم، وملاحظاتهم، والملاحظات الأخرى التي حددها المستخدم: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { attributes: { exclude: [''] } , include:[{ model: Note, attributes: { exclude: ['userId'] } }, { model: Note, as: 'marked_notes', attributes: { exclude: ['userId']}, through: { attributes: [] } }, { model: Team, attributes: ['name', 'id'], through: { attributes: [] } }, ] }) if (user) { res.json(user) } else { res.status(404).end() } }) لا بُد من استخدام الاسم البديل الذي عرّفناه من خلال السمة as خلال السياق. سننشئ بعض البيانات الاختبارية في قاعدة البيانات لاختبار الميزة: insert into user_notes (user_id, note_id) values (1, 4); insert into user_notes (user_id, note_id) values (1, 5); ستكون النتيجة النهائية على النحو التالي: لكن ماذا لو أردنا أن نضمّن معلومات تتعلق بمؤلف الملاحظة إلى الملاحظات التي يحددها مستخدم؟ يُنفَّذ الأمر بإضافة التعليمة include إلى الملاحظات المحدّدة من قبل المستخدم: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { attributes: { exclude: [''] } , include:[{ model: Note, attributes: { exclude: ['userId'] } }, { model: Note, as: 'marked_notes', attributes: { exclude: ['userId']}, through: { attributes: [] }, include: { model: User, attributes: ['name'] } }, { model: Team, attributes: ['name', 'id'], through: { attributes: [] } }, ] }) if (user) { res.json(user) } else { res.status(404).end() } }) ها هي النتيجة النهائية كما نتوقع: يمكنك الحصول على شيفرة التطبيق بوضعه الحالي من المستودع المخصص له على GitHub ضمن الفرع part13-9. التمرينات 13.19 - 13.23 حاول إنجاز التمرينات التالية. التمرين 13.19 اِمنح المستخدمين القدرة على إضافة مدوّنات إلى قائمة قراءة reading list. وعند إضافتها إلى القائمة يجب أن تكون حالتها "غير مقروءة unreaded"، ويمكن لاحقًا تعليم المدوّنة على أنها "مقروءة". نفّذ فكرة قائمة القراءة مستخدمًا جدول اتصال، وعدّل قاعدة البيانات من خلال ملف تهجير. لا يهم في هذا التمرين إضافة مدونات إلى القائمة وعرضها بنجاح أكثر من استخدام فكرة الولوج المباشر إلى قاعدة البيانات. التمرين 13.20 أضف الآن طريقةً كي يدعم التطبيق قوائم القراءة. تُضاف المدوّنة إلى قائمة القراءة من خلال الطلب HTTP POST إلى الوجهة "api/readinglists/"، ويُرفق مع الطلب المدوّنة ومعرّف المستخدم: { "blogId": 10, "userId": 3 } عدّل الوجهة "GET /api/users/:id" لإعادة قائمة المدوّنات إضافة إلى معلومات المستخدم بالتنسيق التالي: { name: "Matti Luukkainen", username: "mluukkai@iki.fi", readings: [ { id: 3, url: "https://google.com", title: "Clean React", author: "Dan Abramov", likes: 34, year: null, }, { id: 4, url: "https://google.com", title: "Clean Code", author: "Bob Martin", likes: 5, year: null, } ] } حتى هذه اللحظة، لا حاجة لإظهار إن كانت المدوّنة مقروءةً أم لا. التمرين 13.21 عدّل الوجهة التي تصل إلى مستخدم وحيد لكي تعرض فيما إذا كانت كل مدوّنة في قائمة القراءة مقروءةً أم لا، إضافةً إلى المُعرَّف المميز "id" للصف المقابل في جدول الاتصال. يمكن عرض المعلومات وفق التنسيق الآتي مثلًا: { name: "Matti Luukkainen", username: "mluukkai@iki.fi", readings: [ { id: 3, url: "https://google.com", title: "Clean React", author: "Dan Abramov", likes: 34, year: null, readinglists: [ { read: false, id: 2 } ] }, { id: 4, url: "https://google.com", title: "Clean Code", author: "Bob Martin", likes: 5, year: null, readinglists: [ { read: false, id: 2 } ] } ] } التمرين 13.22 قدّم طريقةً يستطيع من خلالها التطبيق تعليم مدوّنة ضمن قائمة القراءة على أنها مقروءة، إذ يُنفَّذ الأمر بإجراء طلب PUT إلى الوجهة "api/readinglists/:id/" وإرساله مع القيمة: { "read": true } يمكن للمستخدم أن يعلّم مدوّنة على أنها مقروءةً إذا كانت فقط ضمن قائمة القراءة الخاصة به. يستوثق من المستخدم عادةً من خلال مفتاح الاستيثاق الذي يُرفق مع الطلب. التمرين 13.23 عدّل الوجهة التي تعيد معلومات مستخدم وحيد لكي يتحكم الطلب بالمدوّنة التي ينبغي إحضارها من قائمة القراءة: "GET /api/users/:id": يعيد كامل قائمة القراءة. "GET /api/users/:id?read=true": يعيد المدوّنات المقروءة. "GET /api/users/:id?read=false": يعيد المدوّنات غير المقروءة. ملاحظات عامة يمكننا عدّ حالة تطبيقنا الآن مقبولة، لكن لا بُدّ من إلقاء نظرةٍ على بعض الأفكار قبل أن نختم هذا القسم. إحضار البيانات الكسول lazy والمتلهف eager عندما ننشئ استعلامًا مستخدمين السمة include: User.findOne({ include: { model: note } }) يحدُث ما يُسمى الإحضار المُتلهِّف eager fetch للبيانات، إذ تُجلب جميع الصفوف في كل الجداول المرتبطة بالمستخدم بواسطة الاستعلام join بنفس الوقت في مثال الملاحظات التي يُنشئها مستخدم. هذا السلوك هو ما نحتاجه عادةً، لكن ستجد في المقابل حالات تحتاج فيها إلى ما يُدعى بالإحضار الكسول أو المحدود lazy fetch مثل البحث عن فُرقاء مرتبطةٍ بمستخدم إذا لزم الأمر. لنعدّل وجهة إحضار مستخدم واحد كي تُحضر الفُرقاء التي ينتمي إليها مستخدم إذا احتوى الاستعلام على المعامل teams: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { attributes: { exclude: [''] } , include:[{ model: note, attributes: { exclude: ['userId'] } }, { model: Note, as: 'marked_notes', attributes: { exclude: ['userId']}, through: { attributes: [] }, include: { model: user, attributes: ['name'] } }, ] }) if (!user) { return res.status(404).end() } let teams = undefined if (req.query.teams) { teams = await user.getTeams({ attributes: ['name'], joinTableAttributes: [] }) } res.json({ ...user.toJSON(), teams }) }) وهكذا لن يحضر الاستعلام User.findByPk الفُرقاء، لكنها ستُجلب عند الحاجة باستخدام التابع user.getTeams الذي تولِّده Sequelize تلقائيًا لكائن النموذج. تولَّد Sequelize تلقائيًا توابع -get مماثلة وتوابع أخرى مفيدة عندما تٌعرّف ارتباطات associations بين الجداول على مستوى قاعدة بيانات. ميزات النموذج قد تصادفنا حالات لا نريد فيها معالجة كل أسطر جدول محدد افتراضيًا، إذ من الممكن مثلًا ألا نرغب في عرض المستخدمين الذين أوقفت نشاطاتهم في التطبيق، ويمكننا في هذه الحالة تعريف مجالات الرؤية الافتراضية للنموذج على النحو التالي: class User extends Model {} User.init({ // field definition }, { sequelize, underscored: true, timestamps: false, modelName: 'user', defaultScope: { where: { disabled: false } }, scopes: { admin: { where: { admin: true } }, disabled: { where: { disabled: true } } } }) module.exports = User سيضم الاستعلام الناتج عن التابع ()User.findAll عبارة WHERE التالية: WHERE "user". "disabled" = false; يمكن أن نعرّف أيضًا مجالات رؤية أخرى للنماذج: User.init({ // field definition }, { sequelize, underscored: true, timestamps: false, modelName: 'user', defaultScope: { where: { disabled: false } }, scopes: { admin: { where: { admin: true } }, disabled: { where: { disabled: true } }, name(value) { return { where: { name: { [Op.iLike]: value } } } }, } }) تُستخدم مجالات الرؤية على النحو التالي: // جميع المدراء const adminUsers = await User.scope('admin').findAll() // جميع المستخدمين غير النشطين const disabledUsers = await User.scope('disabled').findAll() // في أسمائهم jami المستخدمون الذين لديهم سلسلة نصية const jamiUsers = User.scope({ method: ['name', '%jami%'] }).findAll() كما يمكن سلسلة مجالات الرؤية (ربطها ببعضها): // في أسمائهم jami المدراء الذين لديهم سلسلة نصية const jamiUsers = User.scope('admin', { method: ['name', '%jami%'] }).findAll() وطالما أن نماذج هي أصناف جافا سكربت JavaScript، من الممكن إضافة توابع جديدة إليها، وإليك مثالين عن ذلك: const { Model, DataTypes, Op } = require('sequelize') const Note = require('./note') const { sequelize } = require('../util/db') class User extends Model { async number_of_notes() { return (await this.getNotes()).length } static async with_notes(limit){ return await User.findAll({ attributes: { include: [[ sequelize.fn("COUNT", sequelize.col("notes.id")), "note_count" ]] }, include: [ { model: Note, attributes: [] }, ], group: ['user.id'], having: sequelize.literal(`COUNT(notes.id) > ${limit}`) }) } } User.init({ // ... }) module.exports = User التابع الأول numberOfNotes هو تابع نسخة instance method، أي أن استدعاءه ممكن من نسخٍ instances عن النموذج: const jami = await User.findOne({ name: 'Jami Kousa'}) const cnt = await jami.number_of_notes() console.log(`Jami has created ${cnt} notes`) تشير الكلمة this في تابع النسخة إلى نسخة النموذج نفسها: async number_of_notes() { return (await this.getNotes()).length } أما التابع الثاني للنموذج، فيعيد هؤلاء المستخدمين الذين يملكون على الأقل "X" وهي القيمة التي يحملها المعامل وتدل على كمية الملاحظات في الصنف، أي التي تُستدعى مباشرةً عن طريق النموذج: const users = await User.with_notes(2) console.log(JSON.stringify(users, null, 2)) users.forEach(u => { console.log(u.name) }) قابلية التكرار في النماذج وملفات التهجير لقد رأينا أن الشيفرة في النموذج أو ملف التهجير تتكرر كثيرًا، فلو أخذنا نموذج الفُرقاء teams: class Team extends Model {} Team.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.TEXT, allowNull: false, unique: true }, }, { sequelize, underscored: true, timestamps: false, modelName: 'team' }) module.exports = Team وملف التهجير فإنهما يضمان كمًّا كبيرًا من نفس الشيفرة: const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.createTable('teams', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.TEXT, allowNull: false, unique: true }, }) }, down: async ({ context: queryInterface }) => { await queryInterface.dropTable('teams') }, } هل يمكن تحسين الشيفرة كي يُصدّر النموذج مثلًا الأجزاء المشتركة التي يحتاجها ملف التهجير؟ تكمن المشكلة في احتمال تغيُّر تعريف النموذج مع الوقت، فقد يتغير مثلًا الحقل أو يتغير نوع البيانات المُخزّنة فيه، وعلى ملف التهجير أن يُنفَّذ بنجاح في أي وقت من البداية إلى النهاية. إذا اعتمَدت ملفات التهجير على النموذج في الحصول على محتوى معين، فقد لا يكون هذا متاحًا خلال شهر أو سنة؛ لهذا ورغم وجود الكثير من الشيفرة للنسخ واللصق، لكن يُعد فصل ملف التهجير عن النموذج كاملًا أمرًا ضروريًا. قد يكون أحد الحلول هو استخدام أداة سطر أوامر Sequelize الذي يولّد كلًا من النموذج وملف التهجير بناءً على الأوامر التي تُنفِّذها، إذ سيُنفِّذ الأمر التالي النموذج User الذي يمتلك السمات name و username و admin، إضافةً إلى ملف التهجير الذي يدير شؤون إنشاء جدول قاعدة البيانات: npx sequelize-cli model:generate --name User --attributes name:string,username:string,admin:boolean يمكننا أيضًا تنفيذ أمر التراجع عن التهجيرات انطلاقًا من سطر الأوامر. لم يكتمل بعد لسوء الحظ توثيق سطر الأوامر هذا، لهذا قررنا إنشاء النماذج وملفات التهجير يدويًا في المنهاج، وقد يكون أو لايكون ما أنجزناه من حلول جيدًا. التمرين 13.24 نهاية عظيمة: أشرنا في نهاية القسم 4 إلى مشكلة جدّية في مفتاح الاستيثاق، فلو قررنا إيقاف نشاط مستخدم بعد أن دخل إلى المنظومة، سيبقى هذا المستخدم قادرًا على استخدام مفتاح الاستيثاق الذي يمتلكه وبالتالي استخدام المنظومة. الحل الإعتيادي للمشكلة هو تخزين سجلات بكل مفتاح استيثاق مُنح إلى عميل في قاعدة بيانات الواجهة الخلفية، ثم التحقق من صلاحية كل طلب، ويمكن في هذه الحالة إزالة صلاحية هذا المفتاح مباشرةً عند الحاجة. يُشار إلى هذا الأسلوب عادةً بجلسة عمل على الخادم server-side session. لنوسّع الآن المنظومة كي يُمنع المستخدم الذي فقد قدرته على الوصول إليها من إجراء أي تفاعل يتطلب تسجيل دخول. قد تحتاج إلى ما يلي لإنجاز الأمر: عمودٌ يضم قيمًا منطقية في جدول المستخدمين يشير إلى كون المستخدم نشطًا أم لا. يكفي في تمريننا أن توقف نشاط مستخدم أو تعيده من خلال قاعدة البيانات مباشرةً. جدولٌ يُخزّن جلسات العمل الجارية تُخزَّن الجلسة عند تسجيل الدخول (عند تنفيذ الطلب "POST /api/login"). يجري التحقق من وجود جلسة أو صلاحيتها عندما يُنفِّذ المستخدم عمليةً تتطلب تسجيل دخول. وجهةٌ تسمح للمستخدم بتسجيل خروجه من المنظومة لإزالة الجلسة من قاعدة البيانات، وقد يكون للوجهة المسار التالي "DELETE /api/logout". تذكر أنه لا يسمح بنجاح أي عملية تتطلب تسجيل دخول إذا كان مفتاح الاستيثاق منتهي الصلاحية، مثل الحالة التي يسجل فيها المستخدم خروجه. قد ترغب أيضًا في استخدام مكتبة npm مخصصة للتعامل مع الجلسات، ولا تنسى استخدام ملف التهجير لتنفيذ التغييرات اللازمة على قاعدة البيانات. ترجمة -وبتصرف- للفصل migrations, many-to-many relationships من سلسلة Deep Dive Into Modern Web Development. اقرأ أيضًا المقال السابق: ضم الجداول والاستعلامات المشتركة في قواعد البيانات باستخدام Sequelize المفاهيم الأساسية في قواعد البيانات وتصميمها تهجير قواعد البيانات في Laravel 5 تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder
  2. تجد في HTML الكثير من العناصر المستخدَمة في تنسيق النصوص ولم نأت على ذكرها في مقال هيكلة النصوص باستخدام لغة HTML، فليست جميع العناصر التي سنذكرها في هذا المقال معروفةً جيدًا، لكن من الجيد الاطلاع عليها (مع ذلك لن تكتمل قائمة العناصر كلها!)، إذ سنتعلم في مقالنا التعامل مع الاقتباسات وقوائم الوصف وطريقة عرض الشيفرات البرمجية والنصوص المتعلقة بها بالإضافة إلى كتابة نصوص مرتفعة عن نسق الكتابة الرئيسي superscript أو منخفضة عنه subscript، وكذلك عرض معلومات التواصل وغيرها. لا بد قبل الشروع في قراءة المقال الاطلاع على أساسيات HTML كما تحدثنا عنها في مقال تعرَّف على لغة HTML وكذلك هيكلة النصوص باستخدام لغة HTML. قوائم الوصف Description lists تعلمنا في مقالات سابقة طريقة إنشاء قوائم HTML بسيطة، لكننا لم نذكر النوع الثالث الذي قد نصادفه أحيانًا وهي قوائم الوصف description lists، إذ يُعَدّ الغرض الرئيسي لهذه القوائم هو تعداد مجموعة من العناصر وإدراج وصف لها أيضًا مثل المصطلحات وتعريفاتها أو الأسئلة وأجوبتها، وإليك مثالًا عن مجموعة من المصطلحات وتعريفها: soliloquy In drama, where a character speaks to themselves, representing their inner thoughts or feelings and in the process relaying them to the audience (but not to other characters.) monologue In drama, where a character speaks their thoughts out loud to share them with the audience and any other characters present. aside In drama, where a character shares a comment only with the audience for humorous or dramatic effect. This is usually a feeling, thought or piece of additional background information تُغلَّف عناصر هذه القائمة داخل عنصر قوائم مختلف وهو <dl>، كما يُغلَّف كل من عناصرها بالعنصر <dt>، في حين يوضَع وصف أو تعريف كل عنصر ضمن العنصر <dd> بعده مباشرةً. مثال عن قائمة وصف سنستخدِم قائمة الوصف لتنسيق النص السابق: <dl> <dt>soliloquy</dt> <dd>In drama, where a character speaks to themselves, representing their inner thoughts or feelings and in the process relaying them to the audience (but not to other characters.)</dd> <dt>monologue</dt> <dd>In drama, where a character speaks their thoughts out loud to share them with the audience and any other characters present.</dd> <dt>aside</dt> <dd>In drama, where a character shares a comment only with the audience for humorous or dramatic effect. This is usually a feeling, thought, or piece of additional background information.</dd> </dl> يعرض المتصفح قائمة الوصف افتراضيًا بحيث ينزاح فيه وصف أو تعريف العنصر قليلًا عن مستوى المصطلح. تعريفات متعددة لمصطلح واحد يُسمح في قوائم الوصف وجود أكثر من وصف أو تعريف لمصطلح، وإليك مثالًا: <dl> <dt>aside</dt> <dd>In drama, where a character shares a comment only with the audience for humorous or dramatic effect. This is usually a feeling, thought, or piece of additional background information.</dd> <dd>In writing, a section of content that is related to the current topic, but doesn't fit directly into the main flow of content so is presented nearby (often in a box off to the side.)</dd> </dl> تطبيق: توصيف مجموعة من التعريفات حان الوقت لتجرّب بنفسك قوائم الوصف، لذلك أضف العناصر المناسبة إلى النص الموجود في حقل مدخلات محرر الشيفرة في الأسفل لكي يظهر على صورة قائمة وصف ضمن حقل المخرجات output field، كما يمكنك تجريب مصطلحات وتعريفات خاصة بك أيضًا. إذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة الضبط Reset، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على زر أظهر الحل Show solution لترى الحل الصحيح. الاقتباسات Quotations تتيح لغة HTML عناصر للدلالة على الاقتباسات quotations، ويختلف العنصر المُستخدَم لإنشاء اقتباس إذا كنت توصِّف اقتباسًا مأخوذًا من عنصر كتلي Block أو سطري Inline. الاقتباسات الكتلية إذا اقتُبس جزء من محتوى عنصر كتلي مثل فقرة نصية أو عدة فقرات أو قائمة من مكان ما، فلا بد من تضمينه داخل العنصر <blockquote> لإظهاره على أساس فقرة وإضافة عنوان URL إلى مصدر الاقتباس بواسطة السمة cite، ويعرض المثال التالي محتوًى مأخوذًا من صفحة العنصر <blockquote> على شبكة مطورِي موزيللا MDN: <p>The <strong>HTML <code><blockquote></code> Element</strong> (or <em>HTML Block Quotation Element</em>) indicates that the enclosed text is an extended quotation.</p> علينا كتابة ما يلي لتحويل هذا المحتوى إلى اقتباس عن عنصر كتلي: <p>Here below is a blockquote...</p> <blockquote cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote"> <p>The <strong>HTML <code><blockquote></code> Element</strong> (or <em>HTML Block Quotation Element</em>) indicates that the enclosed text is an extended quotation.</p> </blockquote> سيصيّر المتصفح هذا الاقتباس افتراضيًا على هيئة فقرة نصية تنزاح قليلًا عن بداية السطر ليدل على أنها اقتباس، وقد وضعنا الفقرة النصية في الأعلى لتوضيح هذه الفكرة. الاقتباس السطري يعمل بطريقة مماثلة للاقتباس عن عنصر كتلي إلا أنه يستخدِم العنصر <q>، إذ يعرض المثال التالي محتوًى مأخوذًا من صفحة العنصر <q> على شبكة مطورِي موزيللا وحوّلناه إلى اقتباس سطري: <p>The quote element — <code><q></code> — is <q cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q">intended for short quotations that don't require paragraph breaks.</q></p> سيعرض المتصفح هذه الاقتباس على صورة نص عادي محاط بإشارتَي تنصيص مزدوجتين على السطر نفسه: الاقتباسات المرجعية Citations على الرغم من أنّ محتوى السمة cite يبدو مفيدًا إلّا أنّ المتصفحات وقارئات الشاشة لا يعيرانه اهتمامًا، إذ لا توجد طريقة مباشرة تطلب بها من المتصفح إظهار محتوى هذه السمة سوى بابتكار حلول خاصة بك من خلال استخدام جافاسكربت أو CSS، فإذا أردت إتاحة مصدر الاقتباس للعرض أو الاستخدام، فعليك إظهاره ضمن نص أو رابط أو أية طريقة ملائمة أخرى، كما يوجد أيضًا العنصر <cite>، إلا أنّ الغاية منه هو احتواء عنوان المصدر الذي أُخذ منه الاقتباس مثل اسم كتاب، لكن لا يوجد أبدًا ما يمنع ربط النص الموجود داخل هذا العنصر بمصدر الاقتباس بطريقة أو بأخرى: <p>According to the <a href="/en-US/docs/Web/HTML/Element/blockquote"> <cite>MDN blockquote page</cite></a>: </p> <blockquote cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote"> <p>The <strong>HTML <code><blockquote></code> Element</strong> (or <em>HTML Block Quotation Element</em>) indicates that the enclosed text is an extended quotation.</p> </blockquote> <p>The quote element — <code><q></code> — is <q cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q">intended for short quotations that don't require paragraph breaks.</q> -- <a href="/en-US/docs/Web/HTML/Element/q"> <cite>MDN q page</cite></a>.</p> تُعرَض الاقتباسات المرجعية افتراضيًا بخط مائل. تطبيق: من صاحب القول؟ حان الوقت لتمرين آخر، انظر إلى النص الموجود في حقل مدخلات محرر الشيفرة في الأسفل وحاول: تحويل المقطع في المنتصف إلى اقتباس كتلي يتضمن السمة cite. تحويل الجملة "The Need To Eliminate Negative Self Talk" في المقطع الثالث إلى اقتباس سطري يتضمن السمة cite. تغليف عنوان كل مصدر داخل العنصر <cite> وتحويل العنوان إلى رابط، واستخدم المرجعين التاليين للربط: للاقتباس عن كونفوشيوس: http://www.brainyquote.com/quotes/authors/c/confucius.html. للجملة في المقطع الثالث: http://example.com/affirmationsforpositivethinking. إذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة الضبط Reset، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على زر أظهر الحل Show solution لترى الحل الصحيح. الاختصارات تُعَدّ الاختصارات Abbreviations عناصر شائعة الاستخدام نوعًا ما في ويب وتميزها بالوسم <abbr>، إذ تستخدَم للدلالة على اختصارات لمصطلحات أو نحوت من عدة كلمات، فالنحت هو الأخذ بأوائل الحروف في كلمات جملة، كما تعطي الاختصارات وصفًا للمصطلح عند تمرير مؤشر الفأرة فوقه عند استخدام السمة title. مثال عن الاختصارات لنلق نظرةً على المثال التالي: <p>We use <abbr title="Hypertext Markup Language">HTML</abbr> to structure our web documents.</p> <p>I think <abbr title="Reverend">Rev.</abbr> Green did it in the kitchen with the chainsaw.</p> ستكون النتيجة مشابهةً للتالي: ملاحظة: دعمَت النسخ الأولى من HTML العنصر <acronym>، لكنه حُذِف ليحل محله <abbr> الذي يُستخدَم للدلالة على الاختصارات والنحوت معًا. تطبيق: الدلالة على اختصار نطلب منك في هذا التمرين البسيط إنشاء اختصار، واستخدم العينة الموجودة في الأسفل أو استبدلها بما تشاء. الدلالة على معلومات التواصل يُستخدَم العنصر <address> في لغة HTML لإدراج معلومات التواصل مع مسؤول الصفحة أو الموقع، إليك مثالًا كما يلي: <address> Chris Mills, Manchester, The Grim North, UK </address> يمكن احتواء العنصر على عناصر أخرى أو نموذج للتواصل لإدراج معلومات أكثر، وإليك مثالًا كما يلي: <address> <p> Chris Mills<br> Manchester<br> The Grim North<br> UK </p> <ul> <li>Tel: 01234 567 890</li> <li>Email: me@grim-north.co.uk</li> </ul> </address> يمكنك تنظيم المعلومات ضمن العنصر <address> بالصورة التالية أيضًا: <address> Page written by <a href="../authors/chris-mills/">Chris Mills</a>. </address> ملاحظة: يجب ألا يستخدَم العنصر <address> سوى لإدراج معلومات التواصل في الصفحة أسفل أقرب عنصر مقال <article> أو أسفل جسم الصفحة <body>، ومن الصحيح أيضًا وضعه في تذييل الصفحة لتُعرض معلومات التواصل في جميع صفحات الموقع، أو داخل عنصر المقال لعرض معلومات التواصل مع مؤلفه، لكن لا تستخدِمه لإدراج قائمة من العناوين التي لا تتعلق بمحتوى الصفحة. إزاحة الكتابة أعلى النسق أو أسفله نحتاج أحيانًا إلى رفع بعض الاحرف إلى أعلى نسق الكتابة أو أسفلها عندما نتعامل مع تواريخ مثلًا أو صيغ المواد الكيميائية أو المعادلات الرياضية لكي تشير إلى المعنى الحقيقي، إذ يُستخدم العنصران <sup> و <sub> لإنجاز الأمر، وإليك مثالًا كما يلي: <p>My birthday is on the 25<sup>th</sup> of May 2001.</p> <p>Caffeine's chemical formula is C<sub>8</sub>H<sub>10</sub>N<sub>4</sub>O<sub>2</sub>.</p> <p>If x<sup>2</sup> is 9, x must equal 3 or -3.</p> سيعرِض المتصفح النتيجة على الصورة التالية: إدراج شيفرة برمجية تقدِّم HTML مجموعةً من العناصر لعرض الشيفرات البرمجية بطريقة مميزة: العنصر <code>: للدلالة على أن المحتوى هو مقتطفات من شيفرة برمجية. العنصر <pre>: الذي يُبقي على المسافات الفارغة الزائدة في المحتوى والتي قد تكون إزاحةً للشيفرة، إذ يتجاهل المتصفح المسافات الفارغة الزائدة في النصوص العادية، لكن عند تغليف النص داخل هذا العنصر، فسيعرضه المتصفح كما هو تمامًا دون إهمال أيّ شيء. العنصر <var>: ويستخدَم للدلالة على المتغيرات على وجه الخصوص. العنصر <kbd>: ويستخدَم للدلالة على مدخلات لوحة المفاتيح أو غيرها من المدخلات إلى الحاسوب. العنصر <samp>: ويستخدَم للدلالة على خرج برنامج حاسوبي. لنلق نظرةً على بعض الأمثلة، كما عليك التجريب بنفسك، ولهذا ننصح بتنزيل نسخة من الملف other-semantics.html. <pre><code>var para = document.querySelector('p'); para.onclick = function() { alert('Owww, stop poking me!'); }</code></pre> <p>You shouldn't use presentational elements like <code><font></code> and <code><center></code>.</p> <p>In the above JavaScript example, <var>para</var> represents a paragraph element.</p> <p>Select all the text with <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>A</kbd>.</p> <pre>$ <kbd>ping mozilla.org</kbd> <samp>PING mozilla.org (63.245.215.20): 56 data bytes 64 bytes from 63.245.215.20: icmp_seq=0 ttl=40 time=158.233 ms</samp></pre> ستُعرض الشيفرة السابقة في المتصفح كما يلي: الدلالة على الوقت والتاريخ تقدِّم لغة HTML العنصر <time> لتوصيف الوقت والتاريخ بطريقة يمكن للآلة قراءتها، وإليك مثالًا كما يلي: <time datetime="2016-01-20">20 January 2016</time> ما الفائدة من هذا العنصر؟ يستخدِم البشر طرقًا مختلفةً لكتابة التاريخ، فقد يُكتب التاريخ في المثال السابق على الصورة: 20 January 2016 20th January 2016 Jan 20 2016 20/01/16 01/20/16 The 20th of next month 20e Janvier 2016 20 كانون الثاني 2016 وغير ذلك. لكن لا تستطيع الحواسب التمييز بسهولة بين طرق الكتابة تلك، فماذا إذا أردت الحصول تلقائيًا على تواريخ جميع المناسبات في صفحة ويب وإضافتها إلى التقويم الخاص بك؟ لهذا السبب، يُستخدَم العنصر <time> لإضافة تنسيق للوقت والتاريخ، بحيث يكون مفهومًا بالنسبة إلى الحواسيب وغيرها من الآلات، كما يعرض لك المثال السابق تاريخًا بسيطًا يمكن للآلة فهمه، لكن هناك خيارات عدة أخرى، وإليك بعض الأمثلة: <!-- تاريخ بسيط بصيغة معيارية --> <time datetime="2016-01-20">20 January 2016</time> <!--فقط السنة و الشهر--> <time datetime="2016-01">January 2016</time> <!-- فقط الشهر و اليوم --> <time datetime="01-20">20 January</time> <!-- الوقت فقط، ساعات ودقائق --> <time datetime="19:30">19:30</time> <!-- يمكنك إضافة الثواني وأجزاء الثواني --> <time datetime="19:30:01.856">19:30:01.856</time> <!-- الوقت والتاريخ --> <time datetime="2016-01-20T19:30">7.30pm, 20 January 2016</time> <!-- الوقت والتاريخ مع إزاحة زمنية حسب خطوط الزمن--> <time datetime="2016-01-20T19:30+01:00">7.30pm, 20 January 2016 is 8.30pm in France</time> <!-- استدعاء أسبوع محدَّد من السنة وفق رقمه --> <time datetime="2016-W04">The fourth week of 2016</time> خلاصة نكون بهذا قد أنهينا دراسة دلالة العناصر التي تنسِّق النصوص في HTML، وتذكَّر تمامًا أنّ العناصر التي تعرَّفت عليها في سلسلة المقالات لا تمثِّل القائمة كلها، فقد حاولنا تغطية الأساسية منها أو بعض العناصر التي يشيع استخدامها أو التي رأينا بأنك ستجدها مهمةً، ويمكنك العودة إلى توثيق لغة HTML باللغة العربية أو شبكة مطوري موزيللا للاطلاع على بقية العناصر، كما سنتحدث في المقال التالي عن عناصر لغة HTML التي نستخدِمها في هيكلة الأجزاء المختلفة من صفحة ويب. ترجمة -وبتصرف- للمقال Advanced text formatting. اقرأ أيضًا HTML و CSS للمبتدئين: مقدمة إلى تنسيقات CSS تنسيق نصوص صفحات الويب باستخدام CSS تنسيق النصوص وتحليلها في PHP كيفية تنسيق النصوص في بايثون 3
  3. يدعم محرر النصوص "مستندات جوجل Google Docs" ميزة مشاركة العمل على أكثر من جهة أو مساهم، مما يسمح بتوزيع حمل العمل على فريق بأكمله، بحيث يختص كل فرد فيه بناحية معينة مثل التحرير والتنقيح وإضافة المصادر والتنسيق والمراجعات العامة بأسلوب عصري مريح يعزز تكامل العمل للحصول على نتائج أفضل. كل ذلك ضمن بيئة عمل متطورة مدعومة بتقنيات سحابية تؤمن سهول الوصول والمشاركة مع مستوى عال من الأمان. سنتحدث في هذا المقال عن ميزة مشاركة مستندات جوجل بتفاصيلٍ وافيةٍ لتكون قادرًا على توزيع أية أعمال تتعلق بإعداد الملفات النصية بفعالية كبيرة وجهد أقل. ولتوضيح النقاط التي ذكرناها، سنعمل عبر مثال تطبيقي بسيط يتضمن إنشاء مستند جوجل دوكس، ثم العمل على مشاركته مع فريقك وتوضيح خيارات المشاركة وآليات العمل الجماعي عليه. إنشاء مستند جديد في مستندات جوجل ينبغي أن يكون لديك بدايةً حساب على منصة جوجل، ويُعَد هذا الحساب وثيقة دخولك إلى جميع الخدمات التي تقدمها المنصة. يرتبط حسابك على منصة جوجل ببريد إلكتروني تُنشئه تلقائيًا عند تسجيل حساب جديد، وعليك تذكره دائمًا مع كلمة السر التي اخترتها لتتمكن من الولوج إلى أي خدمة من خدمات جوجل (علمًا أن بعضها لا يتطلب تسجيل دخول). إن كنت تملك حسابًا، فهذا أمر جيد وإن لم يكن لديك حساب فيجب عليك التسجيل على حساب جديد حتى تتمكن من متابعة العمل معنا في هذا المقال. يمكن الدخول إلى جوجل دوكس من خلال موقع ويب جوجل دوكس مباشرةً، أو من خلال منصة جوجل درايف Google Drive. حاول إنشاء ملف جديد بالنقر على "أيقونة (+)" يمين أسفل الصفحة إن كنت تستخدم موقع مستندات جوجل، كما يمكنك النقر على قائمة "جديد" على يمين الشاشة، ثم النقر على خيار "مستندات Google" إن اخترت أن تعمل على درايف. ولمزيد من التفاصيل عن كيفية إنشاء مستند جوجل، عُد إلى المقال "مقدمة إلى تطبيق مستندات جوجل Google Docs". لنلق نظرةً الآن على واجهة تطبيق مستندات جوجل (واجهة الحاسوب)، التي تتيح لك إمكانية تحرير المستند الجديد الذي أنشأته: تُحفظ مستندات جوجل تلقائيًا بالاسم الافتراضي "بلا عنوان"، لذلك انقر على الخيار "ملف" من شريط القوائم أعلى الصفحة، وانتقل بعدها إلى الخيار "إعادة تسمية" وانقر عليه وستلاحظ وجود عنوان جديد مقترح قد حل مكان العنوان السابق. بإمكانك اعتماد الاسم المقترح أو كتابة العنوان الذي تريد، كما يمكنك النقر مباشرةً على العنوان الافتراضي "بلا عنوان" وكتابة العنوان الجديد للمستند. وبالنسبة لمثالنا التطبيقي، سنختار العنوان "نصائح عن تبادل الأفكار في مستندات جوجل"، وعندها سيُحفظ الملف تلقائيًا في المجلد "ملفاتي". مشاركة مستند جوجل لمشاركة المستند، انقر ببساطة على زر "مشاركة" على يسار الصفحة، وستظهر عندها النافذة التالية: انقر على مربع النص "إضافة أشخاص ومجموعات" أعلى النافذة لكتابة البريد الإلكتروني أو اسم الشخص الذي ترغب بمشاركته هذا الملف (إن كان ضمن قائمة معارفك)، ثم اختر الصفة التي تريده أن يشارك بها من خلال الصندوق الذي يظهر إلى جوار البريد الإلكتروني للشخص المدعو. بإمكانك دعوة أي شخص إلى المساهمة في العمل وفق صفات ثلاث: محرِّر: وتمنحه بذلك وصولًا كاملًا إلى الملف لقراءته وتعديله وترك التعليقات وإضافة مساهمين جدد وتغيير أذونات الوصول (افتراضيًا) وحفظ التغييرات التي أحدثها معلِّق: بإمكان الشخص المدعو الاطلاع على محتوى الملف وترك التعليقات التي يرغب بها دون القدرة على تحرير المحتوى أو تغييره، ويبقى قادرًا أيضًا على استخدام خيارات النسخ والطباعة (افتراضيًا). عارض: لكي تسمح للشخص المدعو أن يقرأ فقط محتوى المستند دون أي قدرة على التفاعل. إن أردت أن تبلغ الشخص بمشاركتك له في العمل على هذا المستند، فانقر مربع التحقق "إشعار الأشخاص"، ثم صُغ رسالة لتوضيح المطلوب منه، بعدها أنقر على الزر "إرسال" أسفل النافذة؛ أما إن لم ترغب بإشعاره بذلك (كأن تبلغه بطريقة أخرى)، فلا تفعّل الخيار "إشعار الأشخاص" وانقر على زر "مشاركة" أسفل النافذة. دعوة شخص لا يملك حساب جوجل: بإمكانك أن تكتب عنوان بريد إلكتروني لشخص لا يمتلك حسابًا على جوجل. وهنا سيبلغك التطبيق في هذه الحالة أن هذا البريد الإلكتروني لا يرتبط بحساب جوجل وأنك تمنح إذنًا لأي شخص يصله رابط المشاركة من خلال هذا البريد، بحيث يترك لك خيار إكمال عملية المشاركة أو إلغائها، إذ يُعَد ذلك ثغرةً أمنيةً بطريقة أو بأخرى. عند إكمال المشاركة تظهر نافذة منبثقة صغيرة أعلى التطبيق تبلغك أن إمكانية الوصول قد تغيّرت، وبإمكانك معرفة عدد الأشخاص الذي تشاركهم العمل من خلال تمرير مؤشر الفأرة فوق زر "مشاركة". انقر على زر المشاركة من جديد لتضيف مساهمًا جديدًا إلى الملف أو لتستعرض المساهمين الحاليين أو لإجراء أية تغييرات على إمكانية الوصول، وستبدو نافذة التحكم بالمشاركة الآن كالتالي: تُظهر النافذة وجود مالك للمستند، وهو بالطبع أنت، إذ يظهر بريدك الإلكتروني وإلى جواره الصفة "مالك"، يليه مساهمين اثنين حتى الآن، حيث يمتلك المساهم الأول البريد الإلكتروني "address@mysite.com" بصفة "معلِّق"، أما الآخر فيمتلك البريد الإلكتروني "address@website.com" بصفة "محرر". تمرين تطبيقي تأكد من قدرتك على مشاركة مستند "نصائح عن تبادل الأفكار في مستندات جوجل" مع ثلاث مساهمين: الأول محرر والثاني معلّق والثالث عارض، إذ ستحتاج ذلك لاحقًا. دعوة شخص تجهل بريده الإلكتروني للمساهمة في مستند جوجل ربما تجمعك علاقة عمل طيبة من زميل، لكنك تجهل في الواقع عنوان البريد الإلكتروني الذي يستخدمه. فإن أردت دعوته للمساهمة في العمل على هذا المستند، يمكنك بكل بساطة اتباع الخطوات التالية: انقر على زر "مشاركة". انقر على زر "نسخ الرابط" أسفل يسار نافذة المشاركة، وسيُخزّن رابط الوصول إلى الملف في الحافظة. أرسل الرابط إلى زميلك بنسخه في رسالة نصية أو عبر إحدى وسائل التواصل الاجتماعي أو بالطريقة التي تشاء. عندما يصل الرابط إلى الشخص المحدد ويحاول الدخول إلى المستند سيتعذر عليه ذلك وتظهر له صفحة تحثّه على طلب إذن منك للمساهمة في الملف. بمجرد أن يطلب إذنًا للوصول إلى مستندك، ستصلك رسالة بريد إلكتروني توضح رغبة هذا الشخص بالمساهمة في العمل على مستندك، وعندما تفتح مستندك ستظهر لك نافذة المشاركة وفيها عنوان البريد الإلكتروني للشخص الذي دعوته للمساهمة كي تحدد الصفة التي تريده أن يساهم بها، وهكذا تكتمل العملية. تعديل إمكانية الوصول إلى مستند جوجل تتيح لك نافذة المشاركة مجموعة من الخيارات التي تساعدك في التحكم بصفات المساهمين ومنح أو إزالة بعض المزايا عن المساهمين عمومًا. السماح بالوصول العام إلى مستند جوجل قد ترغب في نشر مستندك للقراءة أو التعليق أو حتى التعديل من قِبل أكبر شريحة من المهتمين (قد يبدو الأمر محفوفًا بالمخاطر لكنه مفيد في حالات معينة). في هذه الحالة ما عليك سوى نسخ الرابط ونشره عبر الإنترنت، لكن عليك قبل ذلك تغيير حالة المشاركة في قسم "الوصول العام" من "حصري" إلى "أي مستخدم لديه رابط"، ثم اختيار الصفة التي يمكن للمساهمين العامين الدخول بها. تُمنح في هذه الحالة نفس الصفة لجميع المساهمين العامين سواءً اخترتهم أن يكونوا محررين أو معلقين أو عارضين. تعديل صفة الوصول إلى مستند جوجل وإلغائها بإمكانك أن تغير صفة الوصول التي منحتها كمالك أو كمحرر لأحد المساهمين، كأن تمنحه صفة محرر في حال كان "معلّقًا" فقط أو "عارضًا"، والعكس بالعكس؛ بالإضافة إلى منع المساهم من الوصول مجددًا إلى الملف (إزالة الوصول). ولإنجاز الأمر عليك بالخطوات التالية: انقر على زر "مشاركة" لتعرض نافذة التحكم بمشاركة الملف. انقر على القائمة المنسدلة إلى جوار اسم المساهم واختر الصفة الجديدة له أو اختر "إزالة الوصول". انقر على الزر "حفظ" أسفل النافذة. نقل ملكية مستند جوجل يُعًد من أنشأ الملف أو رفعه مالكًا لهذا الملف ويُخزّن ضمن المساحة المخصصة للمالك على جوجل درايف، وعندما يُشارك هذا الملف، ستُرسل نسخة منه إلى المساهم وفق الصفة أو الإذن الممنوح له؛ لكن إن أردت لسبب ما نقل ملكية مستند أنشأته إلى أحد المساهمين، فهذا ممكن وإليك الطريقة: انقر على زر "مشاركة" لتعرض نافذة التحكم بمشاركة الملف. انقر على القائمة المنسدلة إلى جوار اسم المساهم واختر "نقل الملكية". تظهر لك رسالة مفادها أنك ترغب في إرسال دعوة إلى المساهم الذي اخترته ليكون مالكًا لهذا المستند، انقر عند ظهورها على الزر "إرسال الدعوة". بعد إرسال الدعوة ستبقى مالكًا للمستند حتى يصل الإشعار إلى المساهم المحدد ويقبل أن يكون المالك الجديد، عندها تفقد صفة المالك للمستند وتبقى محررًا حتى يغير المالك الجديد صفتك أو يمنعك من الوصول إلى المستند. التراجع عن نقل الملكية: طالما أن المساهم الذي نقلت ملكية الملف إليه لم يستجب لدعوة النقل إيجابًا أو سلبًا، تستطيع التراجع عن نقل الملكية بالنقر على اسم المساهم، ثم اختيار "إلغاء نقل الملكية". الوصول المحدود إلى مستندات جوجل المشاركة يتيح لك تطبيق مستندات جوجل بصفتك مالكًا للمستند إمكانية منع بعض الميزات الناتجة عن مشاركة الملف، وإليك بعضها: منع المساهمين من تنزيل أو نسخ أو طباعة مستند جوجل يمكن للمساهمين الذي يدخلون بصفة "محرر" أو "معلّق" أن ينزّلوا نسخةً من المستند على أجهزتهم أو يطبعوه أو ينسخوا أجزاء منه لأغراض شخصية، لكن بإمكانك منعهم من ذلك كما يلي: انقر على زر "مشاركة" لتعرض نافذة التحكم بمشاركة الملف. انقر على أيقونة الإعدادات في أعلى يسار النافذة لتظهر لك النافذة الجديدة التالية: الغِ تفعيل الخيار "تظهر للمشاركين والمعلقين خيارات التنزيل والطباعة والنسخ". انقر على زر "حفظ". هل هذا كافٍ؟ هذا ما يقدمه تطبيق مستندات جوجل رسميًا، علمًا أن المساهم قد يجد طرقًا كثيرةً للاستفادة من المحتوى وبأساليب مختلفة، لهذا اختر مساهميك بعناية إن كان الأمر مهمًا. منع المساهمين من مشاركة مستندات جوجل وتغيير الأذونات يمكن للمساهم الذي شاركته مستندك بصفة "محرر" أن يعيد مشاركة الملف مع آخرين أو يغيّر أذونات الوصول إليه. ولكي تحتفظ بهذه الميزة لك فقط، اتبع الخطوات التالية: انقر على زر "مشاركة" لتعرض نافذة التحكم بمشاركة الملف. انقر على أيقونة الإعدادات في أعلى يسار النافذة لتظهر لك نافذة الإعدادات السابقة. الغِ تفعيل الخيار "يمكن للمحرِّرين تغيير الأذونات ومشاركة العناصر". انقر على زر "حفظ". حذف مستندات جوجل المشاركة بصفتك مالكًا للمستند يمكنك حذف هذا الملف. وطالما أن الملف محذوف مؤقتًا (موجود في سلة المهملات)، يمكن للمساهمين الذين يمتلكون الأذونات اللازمة العمل على نسخهم من هذا المستند، لكن بمجرد أن تفرغ سلة المهملات سينتهي كل شيء؛ أما إن حذفت ملفًا شاركك به أحد الأشخاص (أي أنك لا تملكه)، فستُحذف نسختك فقط، ولن يؤثر ذلك على عمل بقية المساهمين. بإمكانك استعادة هذا الملف إن حذفته عن طريق الخطأ، وذلك بالوصول إلى الملف من خلال رابطه، ثم اختيار "ملف" يلي ذلك "إضافة إلى ملفاتي". خلاصة غطينا في هذا المقال النقاط التالية: الطرق المختلفة لإنشاء مستند جوجل جديد. طرق مشاركة مستندات جوجل وإضافة أذونات الوصول لمساهمين وإزالتها. تغيير أذونات وصول المساهمين. نقل ملكية مستندات جوجل. الوصول المحدود إلى مستندات جوجل المُشاركة وحذفها. اقرأ أيضًا كيفية المشاركة في كتابة وتعديل مستند باستعمال مستندات جوجل قائمة الأدوات في مستندات جوجل تاريخ النُسخ واستعمال الإضافات في مستندات جوجل
  4. تمتلك الروابط التشعبية أهميةً كبيرةً، فهي العناصر التي تجعل من الويب شبكة حقيقية، إذ نستعرض في هذا المقال الصيغة القواعدية اللازمة لإنشاء رابط، ونناقش أفضل الممارسات المتبعة في إنشائها، كما لا بد قبل المتابعة في قراءة هذا المقال الاطلاع على أساسيات HTML التي تحدثنا عنها في مقال تعرَّف على لغة HTML، وطريقة تنسيق صفحة HTML وهيكلة محتواها كما ورد في مقال هيكلة النصوص باستخدام لغة HTML. ما هو الرابط التشعبي؟ تُعَدّ الروابط التشعبية واحدةً من أكثر الابتكارات أهميةً في عالم الويب، فهي من أولى ميزات الويب منذ انطلاقها وهي بالفعل ما يجعل الويب شبكةً حقيقيةً، إذ تسمح لنا الروابط التشعبية بربط الصفحات بصفحات أخرى أو بأجزاء محددة منها أو بغيرها من الموارد أو الوصول إلى تطبيقات موجودة على عنوان ويب محدَّد، كما يمكن لأيّ محتوى التحول إلى رابط ينقل المستخدِم عند النقر عليه إلى عنوان ويب آخر URL. ملاحظة: يمكن لعنوان URL الإشارة إلى ملف HTML أو ملف نصي أو صور أو ملفات صوت ومقاطع فيديو أو أي شيء آخر على الويب، فإذا لم يتمكن المتصفح من التعامل مع نوع ما من الملفات، فسيسألك إذا كنت تريد فتحه (عندها ينقل مهمة فتح الملف إلى أحد التطبيقات المحلية على جهازك)، أو يُنزّل هذا الملف (لتتعامل معه لاحقًا). تشير الروابط في الصفحة الرئيسية لشبكة BBC مثلًا إلى قصص إخبارية متنوعة إضافية مختلفة، كما تشير إلى مناطق محتلفة من الموقع نفسه (إذ يقدم الموقع آليات للتنقل) وإلى صفحات تسجيل الدخول والاشتراك -أي أدوات المستخدِم- وغير ذلك. تشريح الرابط التشعبي ننشئ الرابط بأبسط أشكاله بتغليف نص أو محتوى معيَّن داخل العنصر <a>، ثم نستخدِم السمة href التي تُعرَف أيضًا باسم مرجع النص التشعبي أو الهدف، والتي تحتوي على العنوان الوجهة. <p>سينقلك هذا الرابط إلى <a href="https://academy.hsoub.com">أكاديمية حسوب</a>. </p> ستكون نتيجة الشيفرة السابقة ما يلي: سينقلك هذا الرابط إلى أكاديمية حسوب. إضافة معلومات داعمة من خلال السمة title قد ترغب أيضًا في إضافة السمة title إلى رابطك، إذ توضع ضمن هذه السمة معلومات إضافيةً عن الرابط مثل نوع المعلومات التي تقدمها الصفحة الهدف، أو الأشياء التي ينبغي الانتباه لها على الموقع. كما تظهر هذه المعلومات على صورة تلميح عند تمرير مؤشر الفأرة فوق الرابط. <p>سينقلك هذا الرابط إلى <a href="https://www.mozilla.org/en-US/" title="أكاديمية حسوب هو موقع تعليمي عربي يهدف إلى توجيه المهتمين العرب بمجال البرمجة و التقنية إلى مادة علمية صحيحة و باللغة العربية" >أكاديمية حسوب</a>. </p> ملاحظة: لا يظهر عنوان الرابط سوى عند تمرير مؤشر الفأرة فوق الرابط، ويعني هذا صعوبة الحصول على معلومات العنوان لمن يستخدِم لوحة المفاتيح أو شاشات اللمس للتنقل عبر صفحات الويب، فإذا كانت معلومات العنوان مهمةً بالفعل لاستخدام الصفحة الهدف، فلا بد من تقديمها بطريقة تمكِّن الجميع من الوصول إليها، كأن تضعها ضمن نص تقليدي. تطبيق: إنشاء رابط خاص بك أنشئ ملف HTML باستخدام القالب الذي نزّلته في مقال تعرف على لغة HTML ومحرِّر الشيفرة على حاسوبك، ثم نفّذ التعديلات التالية: أضف فقرةً نصيةً <p> أو أكثر داخل العنصر <body>، أو غير ذلك من العناصر التي تعلمتها. حوّل أجزاءً من المحتوى إلى روابط. أضف عناوين إلى هذه الروابط باستخدام السمة title. تحويل العناصر البنائية إلى روابط يمكن تحويل كما ذكرنا سابقًا أيّة عناصر إلى روابط حتى العناصر البنائية Block elements، فإذا أردت مثلًا تحويل صورة إلى رابط، فاستخدم العنصر <a> ثم العنصر <img> للدلالة على الصورة. <a href="https://www.mozilla.org/en-US/"> <img src="mozilla-image.png" alt="mozilla logo that links to the mozilla homepage"> </a> جولة سريعة على عناوين URL والمسارات ستحتاج إلى استيعاب عناوين URL ومسارات الملفات لكي تفهم تمامًا وجهة الروابط التشعبية، وهذا ما سنفعله في هذه الفقرة. يُعَدّ محدد موقع المورد Uniform Resource Locator أو URL اختصارًا- سلسلةً نصيةً تُحدِّد مكان تواجد غرض ما على ويب، فالصفحة الرئيسية لموقع حسوب موجودة على العنوان https://www.hsoub.com، إذ تستخدِم عناوين URL المسارات لإيجاد الملفات، في حين تحدد المسارات الموقع الذي يوجد فيه الملف ضمن نظام الملفات، ولنلق نظرةً على هيكلية مجلد: يُدعى المجلد الجذري creating-hyperlinks. سيقع الموقع بأكمله ضمن مجلد واحد عندما نعمل محليًا على موقع ويب، إذ ستجد ضمن المجلد الجذري الملفَين index.html و contacts.html، بحيث سيمثل الملف index.html في موقع الويب الحقيقي الصفحة الرئيسية أو صفحة الهبوط، أي الصفحة التي تُعَدّ نقطة البداية لموقع ويب أو لقسم محدد منه. لاحظ أيضًا وجود مجلدَين ضمن المجلد الجذري هما pdfs و projects، إذ يحتوي كل منهما على ملف وحيد، ولاحظ أنه من الممكن وجود ملفَين باسم index.html في المشروع طالما أنهما في موقعين مختلفين ضمن منظومة الملفات، وقد يكون الملف index.html الثاني الصفحة الرئيسية لمعلومات تتعلق بالمشروع. ملفات في المجلد نفسه: إذا أردت وضع رابط ضمن index.html الأول -أي الأعلى مستوى- يشير إلى الملف contacts.html، فضع اسم هذا الملف فقط لأن كلاهما يعود إلى المستوى نفسه ضمن نظام ترتيب الملفات، وسيكون عنوان URL المستخدم هو contacts.html: <p>Want to contact a specific staff member? Find details on our <a href="contacts.html">contacts page</a>.</p> ملفات في مجلدات فرعية: إذا أردت وضع رابط ضمن index.html الأول -أي الأعلى مستوى- يشير إلى الملف projects/index.html، فعليك الانتقال أولًا إلى المجلد projects ثم الإشارة إلى الملف الذي تريد إنشاء رابط إليه، ويكون ذلك بتحديد اسم المجلد يليه المحرف / ثم اسم الملف، وسيكون عنوان URL الصحيح في هذه الحالة هو projects/index.html: <p>Visit my <a href="projects/index.html">project homepage</a>.</p> ملفات في المجلدات الآباء: إذا أردت وضع رابط ضمن الملف projects/index.html يشير إلى الملف pdfs/project-brief.pdf، فعليك الانتقال إلى المستوى الأعلى مباشرةً ثم الانتقال ثانيةً إلى داخل المجلد pdfs، واستخدم نقطتين متتاليتين .. للانتقال إلى مستوى واحد أعلى، وسيكون العنوان الصحيح هو pdfs/project-brief.pdf/..: <p>A link to my <a href="../pdfs/project-brief.pdf">project brief</a>.</p> ملاحظة: يمكنك الدمج بين الطرق السابقة لكتابة عناوين URL مركّبة عند الحاجة مثل: ../../../complex/path/to/my/file.html الانتقال إلى قسم من صفحة HTML يمكن أن تستهدف من خلال الرابط التشعبي جزءًا محددًا من صفحة HTML -أو ما يعرف بتجزئة الصفحة document fragment- بدلًا من الانتقال إلى أعلى الصفحة، ولإنجاز الأمر لا بد من استخدام السمة id في العنصر الذي ستنتقل إليه في الصفحة المستهدَفة، ومن المنطقي أن تنشئ رابطًا إلى عنصر عنوان مثلًا، إذ سيبدو ذلك قريبًا من التالي: <h2 id="Mailing_address">Mailing address</h2> نضع بعد ذلك قيمة id للعنصر الذي سننتقل إليه في نهاية عنوان URL للصفحة المستهدَفة مسبوقًا بالمحرف #: <p>Want to write us a letter? Use our <a href="contacts.html#Mailing_address">mailing address</a>.</p> يمكن أيضًا استخدام الأسلوب ذاته في الانتقال إلى جزء مختلف من الصفحة نفسها: <p>The <a href="#Mailing_address">company mailing address</a> can be found at the bottom of this page.</p> عناوين URL المطلقة والنسبية ستصادف أثناء تجوالك في ويب مصطلحَي عنوان URL مطلق absolute URL وعنوان URL نسبي relative URL. عنوان URL مطلق يشير إلى موقع محَّدد عن طريق مساره المطلق أو الكامل بما في ذلك الجزء الذي يحدد البروتوكول واسم النطاق. فلو كان الملف index.html ضمن المجلد projects الموجود ضمن المجلد الجذري لموقع ويب التالي: https://www.example.com فسيكون العنوان المطلق إلى هذا الملف هو: https://www.example.com/projects/index.html أو يمكن أن يُكتب فقط على الصورة التالية: https://www.example.com/projects/ إذ معظم الخوادم ستبحث تلقائيًا عن صفحة بداية اسمها index.html، كما يشير العنوان المطلق دائمًا إلى الموقع نفسه أينما استخدِم. عنوان URL نسبي يشير العنوان النسبي إلى موقع ملف بالنسبة إلى الملف الذي تنوي ربطه به، فإذا أردت الربط بين الملف الذي رابطه: https://www.example.com/projects/index.html والملف project-brief.pdf الذي يقع في المجلد نفسه، فسيكون العنوان النسبي هو اسم الملف الهدف كما هو، في حين إذا كان هذا الملف في مجلد فرعي يُدعى pdfs ضمن المجلد نفسه project، فسيكون العنوان النسبي له pdfs/project-brief.pdf، ولاحظ أن المسار المطلق لنفس الملف هو: https://www.example.com/projects/pdfs/project-brief.pdf كما تشير العناوين النسبية إلى مواقع مختلفة تبعًا لموقع الملف الذي سيرتبط بها، فإذا نقلت الملف index.html في مثالنا السابق إلى المجلد الجذري، فسيشير عنوان URL النسبي pdfs/project-brief.pdf إلى الملف الموجود على العنوان التالي: https://www.example.com/pdfs/project-brief.pdf وليس إلى الملف الموجود على العنوان التالي: https://www.example.com/projects/pdfs/project-brief.pdf لم يتغير بالطبع موقع الملف project-brief.pdf ولا موقع المجلد pdfs عندما نقلنا الملف index.html، لكن كل ما هنالك أنّ العنوان النسبي سيشير في هذه الحالة إلى المكان الخاطئ ولن يعمل الرابط عند النقر عليه، فعليك الحذر إذًا. أفضل الممارسات لإنشاء الروابط التشعبية توجد بعض الممارسات الممتازة التي يجدر بك تعلمها لكتابة روابط تشعبية جيدة، لنلق نظرةً عليها. استخدم كلمات واضحة تدل على الرابط من السهل إلقاء الروابط هنا وهناك في صفحتك، وهذا الأمر بالطبع ليس كافيًا، إذ لا بد أن يستطيع جميع القراء الوصول إلى الوجهة الصحيحة أيًا كان وضعهم وأيًا كانت الأدوات التي يستخدمونها، وإليك بعض الأمثلة: يُفضِّل مستخدمِي قارئات الشاشة التنقل من رابط إلى آخر في الصفحة وقراءة هذه الروابط بمعزل عن بقية المحتوى. تستخدِم محركات البحث نص الرابط لفهرسة الملف الذي يستهدفه، فمن الجيد إذًا إضافة كلمات مفتاحية إلى نص الرابط لكي يصف ما يرتبط به بفعالية أكبر. يجول قارئو الصفحات بأنظارهم عبر الصفحة دون قراءة كل كلمة فيها، ويلفت انتباههم ما يبرز بوضوح فيها مثل الروابط، وبالتالي سيشعر المتابع بأهمية الرابط إذا كان النص الوصفي له مفيدًا. الرابط التالي جيد مثلًا: <p><a href="https://firefox.com/"> Download Firefox </a></p> أما هذا، فسيّئ: <p><a href="https://firefox.com/"> Click here </a> to download Firefox</p> تلميحات أخرى: لا تضع عنوان URL لرابط ضمن نص الرابط، إذ سيبدو سيئًا ويصبح أسوأ عندما ينطقه قارئ الشاشة حرفًا حرفًا. لا تكتب "رابط" أو "رابط إلى" ضمن نص الرابط لأنه أمر عبثي، إذ تخبر قارئات الشاشة المستخدِم بأنه رابط، كما يعلم بكل بساطة من يقرأ النص بأنه رابط لأنه سيظهر عمومًا بتنسيق مختلف وبسطر تحته. حاول أن يكون نص الرابط أقصر ما يمكن، فهذا مفيد لمستخدمِي قارئات الشاشة لأنها ستقرأ النص بأكمله. قلل الحالات التي تستخدِم فيها نص الرابط نفسه للدلالة على أماكن مختلفة، فقد يسبب ذلك مشاكل لمستخدمِي قارئات الشاشات إذا ظهرت قائمة من الروابط المتلاحقة التي تقول "انقر هنا". الارتباط بمورد مختلف عن صفحات HTML عند الارتباط بمورد قابل للتنزيل مثل ملف PDF أو وورد، أو بمورد يُتابع مباشرةً مثل ملفات الفيديو والصوت، أو قد يُظهر تأثيرات غير متوقعة مثل فتح نافذة منبثقة أو تحميل مقطع فلاش، فلا بد في هذه الحالات من اختيار كلمات مناسبة للرابط منعًا لأيّ التباس، وإليك بعض الأمثلة: قد يكون حجم باقة التراسل bandwidth connection لديك صغيرًا ثم تنقر فجأةً على رابط لتتفاجأ بتنزيل عدة ميغابايتات لم تتوقعها. قد لا يكون مشغل مقاطع فلاش مثبّتًا على جهازك ثم تنقر رابطًا يأخذك إلى صفحة تتطلب مشغل فلاش. لنلق نظرةً على أمثلة لنصوص يمكن استخدامها في هذه الحالات: <p><a href="https://www.example.com/large-report.pdf"> (PDF ، 10MB) حمل تقرير المبيعات </a></p> <p><a href="https://www.example.com/video-stream/" target="_blank"> (HD سيُعرض في نافذة منفصل وبدقة)‎ شاهد الفيديو </a></p> <p><a href="https://www.example.com/car-game"> (يتطلب مشغل فلاش‎) ‎‎شغِّل لعبة السيارة‎ </a></p> استخدم السمة download عندما ترتبط بمورد لتنزيله يمكن استخدام السمة download عندما تحاول إنشاء رابط إلى مورد لكي يُنزَّل بدلًا من أن يُفتَح في المتصفح، وذلك لكي تزوّد المستخدِم باسم لحفظ الملف، وإليك مثالًا عن رابط لتحميل آخر إصدارات فايرفوكس لنظام التشغيل ويندوز: <a href="https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US" download="firefox-latest-64bit-installer.exe"> Download Latest Firefox for Windows (64-bit) (English, US) </a> تطبيق: إنشاء قائمة للتنقل يُطلَب منك في هذا التمرين ربط بعض الصفحات إلى قائمة تنقّل لإنشاء موقع ويب متعدد الصفحات، وهذه طريقة شائعة لإنشاء المواقع، إذ تُستخدَم الهيكلية نفسها لجميع الصفحات بما في ذلك قائمة التنقّل، وبالتالي عند النقر على أحد الروابط سيعطي ذلك انطباعًا بأنك لازلت في المكان ذاته لكن بمحتوى مختلف. عليك تخزين نسخ من الملفات التالية على حاسوبك وفي المجلد نفسه، كما يمكنك الحصول على جميع هذه الملفات من مستودع جيت-هاب المخصص للتمرين: index.html. projects.html. pictures.html. social.html. عليك: إضافة قائمة غير مرتبة في المكان المطلوب في الصفحة التي تضم أسماء بقية الصفحات التي سترتبط بها، فقائمة التنقل هي عادةً قائمة من الروابط، وبالتالي هذا الأسلوب صحيح دلاليًا. تحويل اسم كل صفحة إلى رابط إلى تلك الصفحة. نسخ قائمة التنقل إلى جميع الصفحات. إزالة الرابط من كل صفحة والذي يتعلق بها، فوجوده أمر غير ضروري ومربك، كما يعطي عدم ظهوره تذكيرًا بصريًا بالصفحة التي نتواجد فيها. سيبدو الحل عندما تنتهي منه قريبًا من الصفحة التالية: ملاحظة: إذا لم تستطع المتابعة أو لم تكن واثقًا من عملك، فيمكن التحقق من الحل في المستودع المخصص للتمرين على جيت-هاب. روابط البريد الإلكتروني من الممكن إنشاء روابط أو أزرار تَفتح عند النقر عليها نموذجًا أو تطبيقًا لكتابة رسالة بريد إلكتروني بدلًا من الانتقال إلى مورد معين. يُستخدم العنصر <a> لهذه الغاية بالإضافة إلى بروتوكول mailto:‎، إذ يشير البروتوكول :mailto في أكثر الحالات بساطةً وشيوعًا إلى عنوان البريد الإلكتروني للمستقبِل: <a href="mailto:nowhere@mozilla.org">Send email to nowhere</a> يُعَدّ عنوان البريد الإلكتروني اختياريًا في واقع الأمر، فإذا أزلت العنوان وأبقيت على البروتوكول :mailto فقط، فستظهر لك نافذة عميل البريد الإلكتروني المثبت على جهازك لإنشاء بريد إلكتروني جديد دون تحديد المستقبِل، ولهذه الميزة فوائدها في روابط المشاركة Share التي يمكن للمستخدِمين النقر عليها لإرسال بريد إلكتروني إلى عناوين من اختيارهم. تحديد بعض التفاصيل يمكن إضافة معلومات أخرى إلى عنوان البريد الإلكتروني، إذ يمكن إضافة أية حقول مستخدَمة في ترويسة البريد الإلكتروني إلى :mailto، وأكثر هذه المعلومات شيوعًا هي الموضوع Subject ونسخة إلى cc وجسم الرسالة body الذي لا يمثل حقلًا فعليًا لنص الرسالة، لكن بالإمكان استخدامه لكتابة محتوى قصير للرسالة، كما يُحدَّد كل حقل مع قيمته بأسلوب الاستعلام، وإليك مثالًا كما يلي: <a href="mailto:nowhere@mozilla.org?cc=name2@rapidtables.com&bcc=name3@rapidtables.com&subject=The%20subject%20of%20the%20email&body=The%20body%20of%20the%20email"> Send mail with cc, bcc, subject and body </a> ملاحظة: ينبغي أن تكون القيم في كل حقل مكتوبةً وفق ترميز عنوان URL، أي دون محارف لا تطبع شيئًا مثل محارف السطر الجديد والجدولة ودون فراغات، ولاحظ أيضًا استخدام إشارة الاستفهام ? في الفصل بين عنوان URL الرئيسي عن قيم الحقول والمحرف & للفصل بين الحقول، إذ تُعَدّ هذه الرموز رموزًا معياريةً لاستعلام URL، وإليك بعض الأمثلة عن عناوين :mailto بسيطة أخرى: mailto: mailto:nowhere@mozilla.org mailto:nowhere@mozilla.org,nobody@mozilla.org mailto:nowhere@mozilla.org?cc=nobody@mozilla.org mailto:nowhere@mozilla.org?cc=nobody@mozilla.org&subject=This%20is%20the%20subject خلاصة هذا كل ما تحتاجه عن الروابط، وسنعود إلى الروابط لاحقًا في سلسلة مقالاتنا عندما نتحدث عن أساليب التنسيق، كما سنتحدث في المقال التالي عن الدلالات التي تقدمها عناصر HTML، وسنطلع على بعض الميزات المتقدمة أو غير المألوفة والتي قد تعُدّها مفيدة، لذا سيكون التنسيق المتقدم للنصوص في لغة HTML هو خطوتنا التالية. احصل على موقع إلكتروني مخصص لأعمالك أبهر زوارك بموقع احترافي ومميز بالاستعانة بأفضل خدمات تطوير وتحسين المواقع على خمسات أنشئ موقعك الآن ترجمة -وبتصرف- للمقال Creating hyperlinks. اقرأ أيضًا مفهوم الروابط التشعبية في مواقع الويب كيفية إنشاء الارتباطات التشعبية (Hyperlinks) والإجراءات (Actions) في Microsoft PowerPoint ترويسة الصفحة والبيانات الوصفية في HTML
  5. نستعرض في هذا المقال طريقة هيكلة التطبيق الذي عملنا عليه في المقال السابق، والاستعلام عن معلومات متنوعة تضمها قاعدة البيانات العلاقية. هيكلية التطبيق لقد كتبنا حتى اللحظة كامل الشيفرة في نفس الملف، لهذا سنحاول الآن إعطاء التطبيق هيكلًا أوضح. لننشئ إذًا المجلدات والملفات وفق الهيكلية التالية: index.js util config.js db.js models index.js note.js controllers notes.js أما محتوى الملفات فستكون على النحو التالي: الملف "util/config.js": يهتم بالتعامل مع متغيرات البيئة environment variables: require('dotenv').config() module.exports = { DATABASE_URL: process.env.DATABASE_URL, PORT: process.env.PORT || 3001, } الملف "index.js": ويهتم بتهيئة وتشغيل التطبيق: const express = require('express') const app = express() const { PORT } = require('./util/config') const { connectToDatabase } = require('./util/db') const notesRouter = require('./controllers/notes') app.use(express.json()) app.use('/api/notes', notesRouter) const start = async () => { await connectToDatabase() app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) }) } start() يختلف تشغيل التطبيق قليلًا عما رأيناه سابقًا، لأننا نريد التأكُّد من نجاح الاتصال بقاعدة البيانات قبل أن يبدأ التطبيق العمل الفعلي. الملف "util/d b.js": ويضم الشيفرة التي تُهيئ قاعدة البيانات: const Sequelize = require('sequelize') const { DATABASE_URL } = require('./config') const sequelize = new Sequelize(DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); const connectToDatabase = async () => { try { await sequelize.authenticate() console.log('connected to the database') } catch (err) { console.log('failed to connect to the database') return process.exit(1) } return null } module.exports = { connectToDatabase, sequelize } الملف "models/note.js": وتُخزّن فيه الملاحظات في النموذج المقابل للجدول الذي سيُحفظ. const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class Note extends Model {} Note.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE } }, { sequelize, underscored: true, timestamps: false, modelName: 'note' }) module.exports = Note الملف "models/index.js": لا يُستخدم حاليًا تقريبًا بسبب وجود نموذج واحد فقط في التطبيق، لكنه سيصبح أكثر فائدةً عندما نبدأ إضافة نماذج جديدة، إذ سيلغي الحاجة إلى إدراج ملفات منفصلة تُعرِّف بقية النماذج: const Note = require('./note') Note.sync() module.exports = { Note } الملف "controllers/notes.js": ويضم الوجهات المرتبطة بالملاحظات، أي مسار التوجيه إلى ملاحظة: const router = require('express').Router() const { Note } = require('../models') router.get('/', async (req, res) => { const notes = await Note.findAll() res.json(notes) }) router.post('/', async (req, res) => { try { const note = await Note.create(req.body) res.json(note) } catch(error) { return res.status(400).json({ error }) } }) router.get('/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { res.json(note) } else { res.status(404).end() } }) router.delete('/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { await note.destroy() } res.status(204).end() }) router.put('/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { note.important = req.body.important await note.save() res.json(note) } else { res.status(404).end() } }) module.exports = router هكذا يبدو هيكل التطبيق جيدًا الآن، لكننا نلاحظ أنّ معالجات التوجيه route handlers الذي يتعامل مع ملاحظة واحدة يضم قليلًا من الشيفرات المكررة، فجميعها تبدأ بالسطر الذي يبحث عن الملاحظة التي يتعامل معها: const note = await Note.findByPk(req.params.id) لنعيد كتابة الشيفرة على شكل أداة وسطية middleware خاصةٍ بنا، ونطبّقها في معالجات التوجيه: const noteFinder = async (req, res, next) => { req.note = await Note.findByPk(req.params.id) next() } router.get('/:id', noteFinder, async (req, res) => { if (req.note) { res.json(req.note) } else { res.status(404).end() } }) router.delete('/:id', noteFinder, async (req, res) => { if (req.note) { await req.note.destroy() } res.status(204).end() }) router.put('/:id', noteFinder, async (req, res) => { if (req.note) { req.note.important = req.body.important await req.note.save() res.json(req.note) } else { res.status(404).end() } }) تستقبل معالجات الوجهة ثلاثة معاملات: الأول نصي يُعرّف الوجهة، والثاني الأداة الوسطية noteFinder المُعرفة مُسبقًا والتي تستخلص الملاحظة من قاعدة البيانات وتضعها في الخاصية note للكائن req. بإمكانك إيجاد الشيفرة الحالية للتطبيق كاملةً في المستودع المخصص على GitHub ضمن الفرع part13-2. التمرينات 13.5 إلى 13.7 حاول إنجاز التمارين التالية التمرين 13.5 غيّر هيكل تطبيقك ليشابه المثال السابق أو اتبع هيكليةً أخرى واضحة وملائمة. التمرين 13.6 قدِّم طريقةً تدعم تغيير عدد الإعجابات بمدوّنة في تطبيقك مستخدمًا العملية "PUT /api/blogs/:id"، إذ ينبغي أن يصل العدد الجديد للإعجابات مع الطلب: { likes: 3 } التمرين 13.7 استخدم أداةً وسطيةً للتحكم المركزي بمعالجة الأخطاء كما فعلنا في القسم 3 كما يمكنك استخدام الأداة الوسطية express-async-errors كما فعلنا في القسم 4. لا تهتم للبيانات المُعادة في سياق رسالة الخطأ. حتى اللحظة لا تتطلب سوى حالتين في التطبيق معالجةً للأخطاء، هما: إضافة مدوّنة جديدة وتغيير عدد الإعجابات، لذلك تأكد من قدرة معالج الأخطاء على التعامل مع هاتين الحالتين بما يلائمهما. إدارة المستخدمين سنضيف تاليًا جدول قاعدة بيانات يُدعى "users" تُخزّن في فيه بيانات مستخدمي التطبيق، كما سنضيف أيضًا وظيفةً لإضافة مستخدمين جدد وآلية تسجيل دخول مبنية على مفاتيح الاستيثاق كما فعلنا في القسم 4، ولكي نبسط العمل، سنُعدِّل ما أنجزناه سابقًا كي يكون لجميع المستخدمين كلمة المرور ذاتها وهي "secret". محتوى الملف "models/user.js" الذي يُعرّف المستخدمين واضحٌ تمامًا: const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class User extends Model {} User.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, username: { type: DataTypes.STRING, unique: true, allowNull: false }, name: { type: DataTypes.STRING, allowNull: false }, }, { sequelize, underscored: true, timestamps: false, modelName: 'user' }) module.exports = User سيكون حقل اسم المستخدم "username" ذا قيم فريدة، لكن وعلى الرغم من إمكانية جعله المفتاح الرئيسي للجدول، إلا أننا قررنا أن ننشئ حقلًا field منفصلًا "id" ذا قيم صحيحة ليكون المفتاح الرئيسي. سيتوسع الملف "models/index.js" قليلًا: const Note = require('./note') const User = require('./user') Note.sync() User.sync() module.exports = { Note, User } لا يضم الملف "controllers/users.js" الذي يحتوي معالجات الوجهة التي تهتم بإنشاء مستخدمين جدد أي شيء مهم حاليًا سوى عرض كل المستخدمين: const router = require('express').Router() const { User } = require('../models') router.get('/', async (req, res) => { const users = await User.findAll() res.json(users) }) router.post('/', async (req, res) => { try { const user = await User.create(req.body) res.json(user) } catch(error) { return res.status(400).json({ error }) } }) router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id) if (user) { res.json(user) } else { res.status(404).end() } }) module.exports = router أما معالج الوجهة الذي يتحكم بتسجيل الدخول (الملف "controllers/login.js") فسيكون على النحو التالي: const jwt = require('jsonwebtoken') const router = require('express').Router() const { SECRET } = require('../util/config') const User = require('../models/user') router.post('/', async (request, response) => { const body = request.body const user = await User.findOne({ where: { username: body.username } }) const passwordCorrect = body.password === 'secret' if (!(user && passwordCorrect)) { return response.status(401).json({ error: 'invalid username or password' }) } const userForToken = { username: user.username, id: user.id, } const token = jwt.sign(userForToken, SECRET) response .status(200) .send({ token, username: user.username, name: user.name }) }) module.exports = router سيُرفق اسم المستخدم وكلمة المرور مع طلب POST، ويُستخلص الكائن المتعلق باسم المستخدم أولًا من قاعدة البيانات باستخدام التابع findOne العائد للنموذج User: const user = await User.findOne({ where: { username: body.username } }) يمكنك أن تلاحظ من خلال الطرفية أن تعليمة SQL المتعلقة باستدعاء التابع السابق هي: SELECT "id", "username", "name" FROM "users" AS "User" WHERE "User". "username" = 'mluukkai'; إن وُجد المستخدم وكانت كلمة المرور صحيحة (وهي "secret" لجميع المستخدمين)، يُعاد مفتاح الاستيثاق "jsonwebtoken" متضمنًا معلومات المستخدم مع الاستجابة. لهذا سنُثبّت الاعتمادية "jsonwebtoken": npm install jsonwebtoken سيتوسع الملف "index.js" قليلًا: const notesRouter = require('./controllers/notes') const usersRouter = require('./controllers/users') const loginRouter = require('./controllers/login') app.use(express.json()) app.use('/api/notes', notesRouter) app.use('/api/users', usersRouter) app.use('/api/login', loginRouter) يمكنك الحصول على شيفرة التطبيق بوضعها الحالي من المستودع المخصص على GitHub ضمن الفرع part13-3. الاتصال بين الجداول يمكن الآن إضافة مستخدمين إلى التطبيق، كما يمكن للمستخدمين تسجيل الدخول، لكنها ليست ميزات مفيدة جدًا في وضعها الحالي، لذلك سنضيف ميزات لا يمكن للمستخدم الاستفادة منها ما لم يُسجل دخوله مثل إضافة ملاحظة جديدة، إذ ترتبط هذه الملاحظة بالمستخدم الذي أنشأها، ولهذا لا بُدّ من إضافة مفتاح خارجي foreign key إلى جدول "notes". يمكن تعريف المفتاح الخارجي عند استخدام مكتبة Sequelize بتعديل الملف "models/index.js" على النحو التالي: const Note = require('./note') const User = require('./user') User.hasMany(Note) Note.belongsTo(User) Note.sync({ alter: true }) User.sync({ alter: true }) module.exports = { Note, User } وهكذا نكون قد عرّفنا علاقة واحد-إلى-متعدد one-to-many تصل بين جدولي المستخدمين "users" والملاحظات "notes"، كما عدّلنا الخيارات في استدعاءات sync لتتطابق جداول قاعدة البيانات مع التغييرات التي حدثت على تعريفات النموذج. سيبدو مخطط قاعدة البيانات على شاشة الطرفية على النحو التالي: username=> \d users Table "public.users" Column | Type | Collation | Nullable | Default ----------+------------------------+-----------+----------+----------------------------------- id | integer | not null | nextval('users_id_seq'::regclass) username | character varying(255) | | not null | name | character varying(255) | | not null | Indexes: "users_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "notes" CONSTRAINT "notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE SET NULL username=> \d notes Table "public.notes" Column | Type | Collation | Nullable | Default -----------+--------------------------+-----------+----------+----------------------------------- id | integer | not null | nextval('notes_id_seq'::regclass) content | text | | not null | important | boolean | | | | date | timestamp with time zone | | | | user_id | integer | | | | Indexes: "notes_pkey" PRIMARY KEY, btree (id) Foreign-key constraints: "notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE SET NULL يشير المفتاح الخارجي user_id الذي أُنشئ في الجدول notes إلى أسطر في الجدول users. لنربط الآن كل ملاحظة جديدة بالمستخدم الذي أنشأها، ولكن قبل تقديم الأمر (ربط الملاحظة بمفتاح استيثاق المستخدم الذي استخدمه لتسجيل دخوله)، لا بُد من كتابة الملاحظة التي ترتبط بالمستخدم الأول الذي يُعثر عليه يدويًا في الشيفرة: router.post('/', async (req, res) => { try { const user = await User.findOne() const note = await Note.create({...req.body, userId: user.id}) res.json(note) } catch(error) { return res.status(400).json({ error }) } }) انتبه إلى وجود العمود user_id في جدول الملاحظات "notes" على مستوى قاعدة البيانات، ويُشار إلى اسم كل صف في قاعدة البيانات بالطريقة التقليدية للمكتبة Sequelize، وذلك بكتابته على نقيض أسلوب سنام الجمل "userId"، أي كما تُكتب تمامًا في الشيفرة (حروف صغيرة). من السهل تنفيذ استعلامات مشتركة في Sequelize، لهذا سنغيّر الوجهة التي تعيد كل المستخدمين لتعرض كل ملاحظات المستخدم: router.get('/', async (req, res) => { const users = await User.findAll({ include: { model: Note } }) res.json(users) }) يُنفَّذ الاستعلام المشترك باستخدام الخيار include مثل معامل استعلام، أما تعليمة SQL المولّدة من الاستعلام، فستُطبع على شاشة الطرفية على النحو التالي: SELECT "User". "id", "User". "username", "User". "name", "Notes". "id" AS "Notes.id", "Notes". "content" AS "Notes.content", "Notes". "important" AS "Notes.important", "Notes". "date" AS "Notes.date", "Notes". "user_id" AS "Notes.UserId" FROM "users" AS "User" LEFT OUTER JOIN "notes" AS "Notes" ON "User". "id" = "Notes". "user_id"; ستبدو النتيجة النهائية كما تتوقع: الإضافة الملائمة للملاحظات لنحاول تغيير طريقة إضافة الملاحظات لتعمل بالطريقة التي عملت بها من قبل، بحيث ينجح إنشاء الملاحظات فقط إذا حمل طلب إنشائها مفتاح استيثاق صحيح عند تسجيل الدخول. تُخزّن بعدها الملاحظة ضمن قائمة الملاحظات التي أنشأها المستخدم المُعرّف بواسطة مفتاح الاستيثاق: const tokenExtractor = (req, res, next) => { const authorization = req.get('authorization') if (authorization && authorization.toLowerCase().startsWith('bearer ')) { try { req.decodedToken = jwt.verify(authorization.substring(7), SECRET) } catch{ res.status(401).json({ error: 'token invalid' }) } } else { res.status(401).json({ error: 'token missing' }) } next() } router.post('/', tokenExtractor, async (req, res) => { try { const user = await User.findByPk(req.decodedToken.id) const note = await Note.create({...req.body, userId: user.id, date: new Date()}) res.json(note) } catch(error) { return res.status(400).json({ error }) } }) يُستخلص مفتاح الاستيثاق من ترويسة الطلب ويفُك تشفيره ويوضع ضمن الكائن req بواسطة الأداة الوسطية tokenExtractor. يُضاف زمن الإنشاء إلى الحقل date أيضًا عند إنشاء الملاحظة. ضبط الواجهة الخلفية تعمل الواجهة الخلفية حتى اللحظة بنفس الطريقة التي تعمل بها في نسخة القسم 4 من التطبيق ماعدا فكرة معالجة الأخطاء. لنغيّر الآن وجهات إحضار جميع الملاحظات وجميع المستخدمين قليلًا قبل توسيع هذه الواجهة. سنضيف إلى كل ملاحظة بعض المعلومات المتعلقة بالمستخدم الذي أنشأها: router.get('/', async (req, res) => { const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: User, attributes: ['name'] } }) res.json(notes) }) كنا قد قيّدنا سابقًا القيم التي يأخذها الحقل المطلوب، إذ كنا نعيد جميع حقول البيانات الخاصة بكل ملاحظة بما في ذلك اسم المستخدم "name" الذي يرتبط بالملاحظة لكن باستثناء حقل المعرّف الفريد للمستخدم "userId". لنجرِ التغيير ذاته على الوجهة route التي تحضر جميع المستخدمين والملاحظات وذلك بإزالة الحقل userId غير الضروري من الملاحظات المرتبطة بمستخدم معين على النحو التالي: router.get('/', async (req, res) => { const users = await User.findAll({ include: { model: Note, attributes: { exclude: ['userId'] } } }) res.json(users) }) يمكنك الحصول على شيفرة التطبيق بوضعها الحالي من المستودع المخصص على GitHub ضمن الفرع part13-4. قليل من الانتباه إلى تعريفات النماذج رغم وجود العمود ‍‍‌‍‍user_id، إلا أننا لم نغيّر النموذج الذي يعرّف الملاحظات، ولكن يمكننا مع ذلك إضافة مستخدم إلى كائن الملاحظة: const user = await User.findByPk(req.decodedToken.id) const note = await Note.create({ ...req.body, userId: user.id, date: new Date() }) يعود السبب وراء ذلك إلى أننا لم نحدّد وجود علاقة واحد-إلى-متعدد في الاتصال بين جدولي المستخدمين "users" والملاحظات "notes" ضمن الملف "models/index.js": const Note = require('./note') const User = require('./user') User.hasMany(Note) Note.belongsTo(User) // ... تُنشئ المكتبة Sequelize تلقائيًا سمةً تُدعى userId في النموذج Note تمنح وصولًا إلى العمود user_id عندما يُشار إليه. وتذكّر أنه يمكنك إنشاء ملاحظة باستخدام التابع build: const user = await User.findByPk(req.decodedToken.id) // إنشاء ملاحظة دون تخزينها بعد const note = Note.build({ ...req.body, date: new Date() }) // للملاحظة المُنشأة userId وضع المعرف الفريد للمستخدم في خاصية note.userId = user.id // تخزين كائن الملاحظة في قاعدة البيانات await note.save() هكذا نرى صراحة أن userId هي سمة attribute لكائن الملاحظات، وقد كان بالإمكان تعريف النموذج على النحو التالي للحصول على النتيجة ذاتها: Note.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE }, userId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, } }, { sequelize, underscored: true, timestamps: false, modelName: 'note' }) module.exports = Note لا يوجد داعٍ للتعريف على مستوى الصنف في النموذج كما فعلنا سابقًا: User.hasMany(Note) Note.belongsTo(User) وبدلًا من ذلك يمكننا تحقيق الأمر بهذه الطريقة، ولا بُد من استخدام إحدى هاتين الطريقتين وإلا لن تُدرك مكتبة Sequelize كيف يتصل الجدولين ببعضهما على مستوى الشيفرة. التمرينات 13.8 إلى 13.11 حاول إنجاز التمارين الآتية: التمرين 13.8 زِد دعم التطبيق لمستخدميه، إذ لا بُد أن يضم جدول المستخدمين الحقول التالية إضافةً إلى الحقل "ID": name: ذو قيمة نصية (لا يمكن أن يكون فارغًا). username: ذو قيمة نصية (لا يمكن أن يكون فارغًا). وعلى خلاف ما أوردنا في الشروحات النظرية، لا تمنع مكتبة Sequelize حاليًا إنشاء البصمتين الزمنيتين create_dat و update_dat لجدول المستخدمين. يمكن إعطاء كلمة المرور نفسها لجميع المستخدمين، وكذلك اختيار طريقة التحقق من كلمة المرور كما في القسم 4، وعليك أيضًا إنجاز الوجهات التالية: "POST api/users": لإضافة مستخدم جديد. "GET api/users": عرض جميع المستخدمين. "PUT api/users/:username": لتغيير اسم المستخدم وليس المعرّف "id". تأكد من إدراج البصمات الزمنية تلقائيًا من قِبل مكتبة Sequelize وعلى النحو الصحيح عند إضافة مستخدم أو تغيير اسمه. التمرين 13.9 تزودنا مكتبة Sequelize بمجموعة قواعد تحقق معرّفةٌ مسبقًا للتأكد من قيم حقول النموذج تُنفّذها قبل تخزين الكائنات في قاعدة البيانات. ولأننا نريد تغيير سياسة إنشاء مستخدم جديد تتطلب أن يكون البريد الإلكتروني المُدخل صحيحًا كما هو حال اسم المستخدم، أنجِز طريقةً للتحقق من ذلك أثناء إنشاء المستخدم. عدّل أيضًا بالأداة الوسطية المستخدمة في معالجة الأخطاء لتعرض وصفًا أكثر وضوحًا لرسالة الخطأ مثل استخدام رسائل خطأ Sequelize على سبيل المثال. { "error": [ "Validation isEmail on username failed" ] } التمرين 13.10 وسّع التطبيق لكي تربط كل مستخدم سجّل دخوله بنجاح عبر مفتاح الاستيثاق بكل مدونة يضيفها. لا بُد من تقديم وصلة endpoint تسجيل دخول "POST /api/login" تُعيد مفتاح الاستيثاق. التمرين 13.11 تأكد أن المستخدم قادرٌ فقط على حذف المدونات التي يضيفها. التمرين 13.12 عدّل الوجهات كي تكون قادرًا على استخلاص جميع المدوّنات والمستخدمين كي تعرض كل مدوّنة المستخدم الذي أضافها وأن يعرض كل مستخدم المدوّنات التي أضافها. استعلامات أوسع لا يزال تطبيقنا حتى اللحظة بسيطًا من ناحية الاستعلامات التي يجريها، إذ اقتصر البحث على صف واحد بناءً على مفتاح رئيسي من خلال التابع "findByPk"، أو البحث عن كل الصفوف باستخدام التابع "findAll". كانت هذه الاستعلامات كافيةً بالنسبة للواجهة الأمامية في نسخة القسم 5 من التطبيق، لكننا سنوّسع الواجهة الخلفية لنتمكن من تنفيذ استعلامات أعقد قليلًا. لننجز أولًا طريقة للحصول على ملاحظات مصنّفة على أنها مهمة أو غير مهمة فقط، باستخدام معامل الاستعلام important: router.get('/', async (req, res) => { const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where: { important: req.query.important === "true" } }) res.json(notes) }) وهكذا ستتمكن الواجهة الخلفية من استخلاص الملاحظات المهمة عند وصول الطلب: http://localhost:3001/api/notes?important=true والملاحظات غير المهمة عند وصول الطلب: http://localhost:3001/api/notes?important=false يتضمن استعلام SQL الذي ولّدته Sequelize العبارة WHERE، التي تُرشّح الصفوف المُعادة في الحالة الطبيعية: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" = true; وبالطبع لن ينفع إنجاز الأمر بهذه الطريقة ما لم يهتم الاستعلام بأهمية الملاحظة، كأن يكون على النحو التالي: http://localhost:3001/api/notes يمكن تدارك الموضوع بطرق عدة، أولها -وقد لا تكون الطريقة الأفضل- على النحو التالي: const { Op } = require('sequelize') router.get('/', async (req, res) => { let important = { [Op.in]: [true, false] } if ( req.query.important ) { important = req.query.important === "true" } const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where: { important } }) res.json(notes) }) يُخزّن الكائن important الآن شرط الاستعلام، وسيكون الشكل الافتراضي لهذا الاستعلام على النحو التالي: where: { important: { [Op.in]: [true, false] } } أي أن العمود قد يحمل إحدى القيمتين true أو false باستخدام العامل Op.in، وهو أحد عوامل Sequelize العديدة؛ فإذا حددنا قيمة للمعامل req.query.important، سيتغير الاستعلام ليصبح بأحد الشكلين التاليين: where: { important: true } أو where: { important: false } بناءً على قيمة معامل الاستعلام. يمكن زيادة القدرة الوظيفية للتطبيق بالسماح للمستخدم بتخصيص كلمة مفتاحية لإحضار البيانات، إذ يعيد الطلب التالي: http://localhost:3001/api/notes?search=database الملاحظات التي تشير إلى الكلمة "database". كما يعيد الطلب التالي: http://localhost:3001/api/notes?search=javascript&important=true جميع الملاحظات التي حُددت أنها مهمة "important" وتشير إلى الكلمة "javascript". سيكون تنفيذ الأمر على النحو التالي: router.get('/', async (req, res) => { let important = { [Op.in]: [true, false] } if ( req.query.important ) { important = req.query.important === "true" } const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where: { important, content: { [Op.substring]: req.query.search ? req.query.search : '' } } }) res.json(notes) }) يوّلد التابع Op.substring الاستعلام الذي نريده باستخدام كلمة "LIKE"، فإذا أنشأت الاستعلام التالي مثلًا: http://localhost:3001/api/notes?search=database&important=true فسيولّد استعلام SQL ما نتوقعه تمامًا: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" = true AND "note". "content" LIKE '%database%'; لكن لا تزال هناك ثغرة مزعجة تتمثل في أن الطلب التالي: http://localhost:3001/api/notes والذي يعني أننا نريد الحصول على جميع الملاحظات، سيتسبب في وجود عبارة WHERE غير الضرورية في الاستعلام، والتي قد تؤثر في فعاليته وفقًا لآلية عمل محرك قاعدة البيانات: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" IN (true, false) AND "note". "content" LIKE '%%'; لنحسّن الشيفرة لكي تُستخدم عبارة WHERE عند الحاجة فقط: router.get('/', async (req, res) => { const where = {} if (req.query.important) { where.important = req.query.important === "true" } if (req.query.search) { where.content = { [Op.substring]: req.query.search } } const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where }) res.json(notes) }) وإذا احتوى الطلب على شرط للبحث مثل: http://localhost:3001/api/notes?search=database&important=true سيتولد استعلام يضم عبارة WHERE: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" = true AND "note". "content" LIKE '%database%'; وإذا لم يحتوي شرطًا، فلن يكون لوجود WHERE حاجة: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id"; يمكنك الحصول على شيفرة التطبيق بوضعها الحالي من المستودع المخصص على GitHub ضمن الفرع part13-5. التمرينات 13.13 إلى 13.16 حاول إنجاز التمرينات التالية: التمرين 13.13 قدِّم آليةً لترشيح filtering النتائج من خلال كلمة مفتاحية ضمن الوجهة التي تعيد كل المدوّنات، وينبغي أن تعمل الآلية على النحو التالي: "GET /api/blogs?search=react": تعيد كل المدونات التي تضم الكلمة "react" في الحقل علمًا أن البحث حساس لحالة الأحرف. "GET /api/blogs": يعيد جميع المدوّنات. يمكنك الاستفادة من عوامل Sequelize لتنفيذ التمرين. التمرين 13.14 وسّع آلية الترشيح للبحث عن كلمة مفتاحية في أحد الحقلين "field" أو "author"، إذ سيعيد الاستعلام: GET /api/blogs?search=jami كل المدوّنات التي تضم الكلمة في أحد الحقلين "field" أو "author". التمرين 13.15 عدّل وجهات المدوّنات لتعيد المدوّنات بناءً على عدد الإعجابات بترتيب تنازلي. اطلع من خلال توثيق Sequelize على تعليمات ترتيب نتائج الاستعلام. التمرين 13.16 أنشئ الوجهة "api/authors/" كي تعيد عدد المدونات التي أضافها المؤلف والعدد الكلي للإعجابات. قدّم العملية على مستوى قاعدة البيانات. قد تحتاج إلى وظيفة التجميع group by ودالة التجميع sequelize.fn. قد تبدو المعلومات المُعادة بتنسيق JSON على غرار المثال الآتي: [ { author: "Jami Kousa", articles: "3", likes: "10" }, { author: "Kalle Ilves", articles: "1", likes: "2" }, { author: "Dan Abramov", articles: "1", likes: "4" } ] مهمة لعلامة إضافية: رتِّب البيانات المُعادة بناءً على عدد الإعجابات، على أن تُنفّذ عملية الترتيب من خلال استعلام قاعدة البيانات. ترجمة -وبتصرف- للفصل Join tables and queries من سلسلة Deep Dive Into Modern Web Development اقرأ أيضًا المقال السابق: العمل مع قواعد بيانات علاقية باستخدام Sequelize النماذج واﻻستعلامات والتقارير في برنامج قواعد بيانات مايكروسوفت أكسس Microsoft Access التعامل مع قواعد البيانات تصميم الجداول ومعلومات المخطط وترتيب تنفيذ الاستعلامات في SQL
  6. قد يحصل المخترق على مفتاح التشفير، لكن لن يستطيع استخدامه لأسباب كثيرة.
  7. أحد المهام الرئيسية للغة HTML هي هيكلة النص لكي يتمكن المتصفح من عرض صفحات ويب بالطريقة التي يريدها المطوّر، إذ يصف هذا المقال طريقة استخدام لغة HTML لهيكلة صفحة نصية بإضافة عناوين وفقرات وإبراز بعض العبارات والكلمات وإنشاء قوائم وغير ذلك. لا بدّ قبل الشروع في القراءة من الاطلاع على أساسيات لغة HTML التي يغطيها المقال تعرَّف على لغة HTML التعامل مع الأساسيات: العناوين الرئيسية والفقرات تتكون معظم النصوص المهيكلة من عناوين وفقرات سواء أكان النص قصة أو صحيفة أو كتاب جامعي أو مجلة. تسهل النصوص المهيكلة تجربة القراءة وتزيد المتعة، وتُنظَّم النصوص ضمن فقرات في لغة HTML بتغليف النص داخل عنصر الفقرة النصية <p> كما يلي: <p>أكتب حاليًا فقرة نصية</p> بينما لا بد من استخدام عنصر عنوان مثل العنوان الرئيسي <h1> لإبراز النص على أساس عنوان للمحتوى الذي تعرضه الصفحة: <h1>هذا عنوان المقال</h1> تقدّم لغة HTML ستة مستويات للعناوين ابتداءً من الأعلى مستوى <h1> وانتهاءً بالأدنى<h6>، إذ يمثِّل العنصر <h1> العنوان الرئيسي ويمثِّل <h2> العناوين الفرعية للعنوان الرئيسي و<h3> العناوين الفرعية للعناوين الفرعية وهكذا، انظر توثيقها -العناصر h1-h6- في موسوعة حسوب. إنجاز الهيكلية المتدرجة لمحتوى صفحة HTML سنستخدِم في القصة التي نراها تاليًا العنصر <h1> ليمثل عنوان القصة و<h2> ليمثل عنوان كل فصل، بينما سيمثل العنصر <h3> الأقسام الفرعية لكل فصل. <h1>The Crushing Bore</h1> <p>By Chris Mills</p> <h2>Chapter 1: The dark night</h2> <p>It was a dark night. Somewhere, an owl hooted. The rain lashed down on the ...</p> <h2>Chapter 2: The eternal silence</h2> <p>Our protagonist could not so much as a whisper out of the shadowy figure ...</p> <h3>The specter speaks</h3> <p>Several more hours had passed, when all of a sudden the specter sat bolt upright and exclaimed, "Please have mercy on my soul!"</p> يعود الأمر إليك في طبيعة الحال لاختيار عناصر العناوين ودلالاتها طالما أنّ التسلسل الذي تتبعه في هيكلة المحتوى منطقي، لذلك لا بد أن تتذكر دائمًا بعض الممارسات الجيدة عند هيكلة العناوين: يفضَّل استخدام عنوان رئيسي <h1> واحد في كل صفحة، فهو العنوان ذو المستوى الأعلى وتتدرج تحته بقية العناصر. تأكد من استخدام عناصر العناوين بالترتيب الصحيح في الهيكلية المتدرجة للصفحة، فلا تستخدِم مثلًا <h3> ليشير إلى عنوان فرعي ثم تستخدم بعده <h2> ليمثل عنوانًا فرعيًا لعنوان فرعي، فهذا أمر غير منطقي وسيقود إلى نتائج غريبة. حاول عدم استخدام أكثر من ثلاثة مستويات في كل صفحة إلا إذا كان الأمر ضروريًا، فقد قَلَّ انتشار المستندات التي تعتمد هيكلية ذات مستويات عميقة نظرًا لصعوبة التنقل بين مستوياتها، ويفضل في حالات مثل هذه توزيع المحتوى ضمن صفحات عدة. ما فائدة إعطاء الصفحة هيكلية معينة؟ للإجابة على هذا السؤال، دعنا نلقي نظرةً على الملف text-start.html الذي سيكون نقطة انطلاق في تطبيقات هذا المقال (وصفة حمُّص جيدة)، لذلك انسخ هذا الملف وضعه على حاسوبك لأنك ستحتاجه لاحقًا، كما يحتوي جسم المستند محتوًى مكوّنًا من أجزاء مختلفة لم تُنظَّم بعد في هيكلية محددة، لكنها مفصولة عن بعضها بمحارف الأسطر الجديدة، فعندما تفتح الملف عبر المتصفح، فستجد أنّ النص قطعة واحدة كبيرة. السبب في ذلك هو عدم وجود أيّة عناصر لهيكلة هذا النص، فلن يميّز المتصفح بين العناوين أو الفقرات، بالإضافة إلى ذلك: يستعرض الزائر النص بسرعة بحثًا عن المحتوى الأساسي ويكتفي عادةً بقراءة العناوين ليختار نقطة البداية، إذ يقضي المستخدِم عادةً فترةً قصيرةً جدًا ضمن صفحة الويب، فإذا لم يستطع الزائر إيجاد أيّ شيء واضح أو مفيد خلال ثوان معدودة، فعلى الغالب سيتوقف عن القراءة ويغادر الصفحة. تفهرِس محركات البحث الصفحات آخذةً بالحسبان محتوى العناوين على أنها كلمات مفتاحية مهمة تؤثِّر على ترتيب نتائج البحث، فدون العناوين إذًا لن تُعَدّ صفحتك جيدةً وفق معايير السيو SEO أو تحسين محركات البحث Search Engine Optimization. لا يقرأ الزوار ذوو المشاكل البصرية الحادة صفحات ويب بأنفسهم، وإنما يستمعون إلى محتواها عن طريق برمجيات تُدعى قارئات الشاشة، إذ تتيح هذه البرمجيات الوصول السريع إلى محتوى نصي محدد، وتجري هذه العملية بطرق عدة منها تحديد خطوط عريضة للمحتوى بقراءة العناوين، مما يسمح للمستخدِم بإيجاد المعلومات التي يريدها بسرعة، فإذا لم تكن العناوين موجودةً، فسيضطر المستخدِم إلى الاستماع إلى النص بأكمله. إذا أردت تنسيق الصفحة باستخدام CSS أو جعلها تفاعليةً أكثر عن طريق جافاسكربت، فلا بد من تغليف المحتوى المطلوب بواسطة عناصر HTML لكي تستهدفه شيفرات CSS أو جافاسكربت بفعالية أكبر. لهذه الأسباب وغيرها سنحتاج إلى هيكلة محتوى الصفحة. تطبيق: هيكلة محتوى لننتقل مباشرةً إلى التطبيق العملي، إذ سنضيف عناصر إلى النص غير المنسق الموجود في حقل المدخلات Input field والذي يُعرض في حقل المخرجات Output field، فإذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر "إعادة ضبط Reset"، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على الزر"أظهر الحل Show solution" لترى الحل الصحيح. لماذا نحتاج إلى الدلالات؟ نعتمد على الدلالات في كل مكان حولنا، فنحن نعتمد على تجاربنا السابقة لتدلنا على وظيفة كل الأغراض التي حولنا، فعندما نرى هذا الغرض نعرف تمامًا عمله، فنحن نتوقع مثلًا أن تعني إشارة المرور الحمراء التوقف والخضراء الحركة، لكن سنرتبك بالفعل إذا تغيّرت الدلالات أو طبقناها بصورة خاطئة. لا بد في السياق عينه أن نتأكد من استخدام العناصر الصحيحة لكي نعطي المحتوى المعنى والوظيفة والمظهر الصحيح، فوفقًا لهذه المقاربة، فإن العنصر <h1> هو عنصر دلالي يعطي المحتوى الذي يغلفه صفة عنوان رئيسي لصفحتك بأعلى مستوى. <h1>This is a top level heading</h1> سيعطي المتصفح محتوى هذا العنوان حجمًا كبيرًا افتراضيًا لكي يظهر على أساس عنوان رئيسي (على الرغم من أنك قادر على تنسيقه بالطريقة التي تريد). والأهم من ذلك أن لدلالته استخدامات عدة في محركات البحث مثلًا أو في قارئات الشاشة، كما يمكنك من ناحية أخرى عرض أيّ عنصر وكأنه عنوان رئيسي. لاحظ الشيفرة التالية: <span style="font-size: 32px; margin: 21px 0; display: block;">Is this a top level heading?</span> ليس للعنصر <span> أية دلالات، ويستخدَم لتطبيق تنسيق لغة CSS على محتوى محدد أو جزء منه أو استهدافه بشيفرة جافاسكربت دون أن يدل ذلك على معنى جديد، وقد طبقنا في الشيفرة السابقة بعض التنسيقات على محتوى ليبدو تمامًا على أنه عنوان رئيسي، لكنك لن تجني أيّ فوائد أخرى من هذه العملية لأنّ هذا العنصر غير دلالي ولا يحمل معنًى محددًا، لذلك من الأفضل دائمًا استخدام عناصر HTML المخصصة لأداء وظيفة معينة. القوائم لنركِّز الآن على القوائم، فهي موجودة في كل مكان يحيط بنا ابتداءً من قائمة التسوق إلى قائمة الاتجاهات التي تسلكها يوميًا للوصول إلى المنزل، وستجد كذلك أنّ القوائم موجودة في كل مكان على الويب ويهمك منها ثلاثة أنواع سنناقشها بشيء من التفصيل. القوائم غير المرتبة تستخدَم القوائم غير المرتبة Unordered list لوصف مجموعة عناصر لا أولوية لترتيبها ضمن القائمة، ومن الأمثلة عليها قائمة التسوق: milk eggs bread hummus تبدأ القائمة غير المرتبة بالعنصر <ul> الذي يغلف جميع العناصر: <ul> milk eggs bread hummus </ul> وينبغي أن نغلف عناصر القائمة بالعنصر <li>: <ul> <li>milk</li> <li>eggs</li> <li>bread</li> <li>hummus</li> </ul> تطبيق: إنشاء قائمة غير مرتبة حاول استخدام محرر الشيفرة في الأسفل لإنشاء قائمة غير مرتبة خاصة بك: القوائم المرتبة تستخدَم القوائم المرتبة ordered list لوصف مجموعة عناصر ينبغي ترتيبها ضمن القائمة، ومن الأمثلة عليها الاتجاهات التي يجب أن تسلكها لتصل إلى مكان معيَّن: Drive to the end of the road Turn right Go straight across the first two roundabouts Turn left at the third roundabout The school is on your right, 300 meters up the road توصَّف القائمة المرتبة بأسلوب القوائم غير المرتبة نفسه باستثناء العنصر المستخدَم في تغليف العناصر الذي سيكون في هذه الحالة <ol> بدلًا من <ul>: <ol> <li>Drive to the end of the road</li> <li>Turn right</li> <li>Go straight across the first two roundabouts</li> <li>Turn left at the third roundabout</li> <li>The school is on your right, 300 meters up the road</li> </ol> تطبيق: إنشاء قائمة مرتبة حاول استخدام محرر الشيفرة في الأسفل لإنشاء قائمة مرتبة خاصة بك: تطبيق: هيكلة وصفة الطعام في مثالنا السابق أصبحت في هذه المرحلة قادرًا على هيكلة الصفحة التي تتحدث عن وصفة الحمُّص في مثالنا السابق، إذ يمكنك تخزين نسخة عن الملف text-start.html على جهازك ثم تجري التعديلات عليه، أو تنفيذ الأمر مستخدمًا محرر الشيفرة في الأسفل، وقد يكون تنفيذ التمرين على حاسوبك أفضل لأنك ستتمكن من حفظ عملك، في حين لن تتمكن من ذلك إذا استخدمت محرر الشيفرة في المقال لأن التغييرات التي أجريتها ستختفي عند إغلاق الصفحة. إذا وجدت نفسك تائهًا، فانقر على الزر "أظهر الحل Show solution" لترى الحل الصحيح، أو تحقق من الملف text-complete.html الذي يحتوي على العمل بأكمله. القوائم المتداخلة من الممكن جدًا أن تتداخل القوائم، فقد تحتاج إلى قائمة فرعية تحت أحد عناصر قائمة أخرى، ولنأخذ القائمة الثانية من مثال وصفة الطعام: <ol> <li>Remove the skin from the garlic, and chop coarsely.</li> <li>Remove all the seeds and stalk from the pepper, and chop coarsely.</li> <li>Add all the ingredients into a food processor.</li> <li>Process all the ingredients into a paste.</li> <li>If you want a coarse "chunky" hummus, process it for a short time.</li> <li>If you want a smooth hummus, process it for a longer time.</li> </ol> بما أن آخر عنصرين يتعلقان بالموضوع ذاته، فقد يبدوان مثل إرشادات فرعية تحت العنصر الذي يسبقهما، ومن المنطقي في هذه الحالة وضعهما ضمن قائمة غير مرتبة فرعية تحت العنصر الذي يسبقهما: <ol> <li>Remove the skin from the garlic, and chop coarsely.</li> <li>Remove all the seeds and stalk from the pepper, and chop coarsely.</li> <li>Add all the ingredients into a food processor.</li> <li>Process all the ingredients into a paste. <ul> <li>If you want a coarse "chunky" hummus, process it for a short time.</li> <li>If you want a smooth hummus, process it for a longer time.</li> </ul> </li> </ol> عُد إلى التطبيق السابق وحاول تعديل القائمة الثانية كما فعلنا هنا. إبراز النص والإشارة إلى أهميته نحاول أحيانًا إبراز بعض الكلمات عندما نتحدث للإيحاء بأهميتها أو لنعني أمرًا آخر، وقد نحاول إعطاء أهمية لكلمات أخرى أوتمييزها بطريقة أو بأخرى، وكذلك الأمر في HTML، إذ تضم اللغة عناصر دلالية لتمييز المحتوى النصي بأسلوب مماثل، وسنناقش تاليًا أكثر هذه العناصر شيوعًا. إبراز محتوى عندما نريد إبراز أو تفخيم عبارة في الكلام المحكي نشدِّد على كلمات محدَّدة لكي نغيّر عمدًا دلالة ما نقول، وهذا ما نفعله أيضًا أثناء الكتابة، إذ نميل إلى التشديد على الكلمات بكتابتها على نحو مائل، وتحمل الجملتين التاليتين على سبيل المثال دلالتين مختلفتين: سعيد لأنك لم تتأخر سعيد لأنك لم تتاخر تشير الجملة الأولى إلى راحة الشخص بأن الآخر لم يتأخر بالفعل، بينما تحمل الثانية نوعًا من الاستهزاء أو هجوم مبطن على الآخر للتعبير عن الانزعاج من تأخره. نستخدِم في HTML العنصر <em> لتوصيف حالات مثل هذه وزيادة المتعة أثناء قراءة النص، كما تُميِّز قارئات الشاشة هذه العناصر وتنطقها بنبرة صوتية مختلفة، وتعرض المتصفحات محتوى هذا العنصر بخط مائل افتراضيًا، لكن انتبه إلى استخدام العنصر لمجرد عرض النص مائلًا، وعليك عندها استخدام العنصر <i> أو أن تطبق تنسيق CSS مع عنصر بصنف محدد. <p>I am <em>glad</em> you weren't <em>late</em>.</p> إظهار أهمية محتوى لإبراز أهمية بعض الكلمات، فإننا نشدِّد عليها عند لفظها أو نكتبها بخط ثخين Bold مثل هذا السائل عالي السميّة. نستخدِم في HTML العنصر <strong> لإظهار أهمية الكلمات، إذ يعرض المتصفح محتوى هذا العنصر بخط ثخين افتراضيًا، كما تميزه قارئات الشاشة وتنطقها بنبرة صوتية مختلفة، ولا تحاول أيضًا استخدام العنصر لمجرد عرضه بخط ثخين، وعليك عندها استخدام العنصر <b> أو تطبيق تنسيق CSS مع عنصر بصنف محدد. <p>This liquid is <strong>highly toxic</strong>.</p> <p>I am counting on you. <strong>Do not</strong> be late!</p> يمكنك أيضًا المزج بين العنصرين <strong> و <em>. <p>This liquid is <strong>highly toxic</strong> — if you drink it, <strong>you may <em>die</em></strong>.</p> تطبيق: لنغير أهمية محتوى يوجد نص قابل للتعديل في محرِّر الشيفرة التالي، وحاول أن تبرز بعض الكلمات وتعطي أهمية لبعضها الآخر، كما ترى لكي تتمرن قليلًا: خط مائل أو ثخين أو تحته سطر تتميز العناصر التي تحدثنا عنها سابقًا بدلالات معنوية محددة، لكن حالة العناصر <b> و <i> و <u> أعقد قليلًا، فقد وضعت هذه العناصر كي يكتب الأشخاص كلمات محددة بخط ثخين أو مائل أو ليظهر تحتها سطر، وذلك في حقبة لم تكن تنسيقات CSS مدعومة أو كانت محدودة الدعم، إذ تؤثر هذه العناصر على طريقة العرض وليس على الدلالة، وتُعرف حاليًا بعناصر العرض presentational elements، كما لا ينبغي استخدام هذه العناصر حاليًا لأنها غير دلالية، وقد رأينا أهمية الدلالات على أصعدة مختلفة مثل سهولة الوصول accessibility وتحسين محركات البحث أو سيو SEO. أعادت HTML5 تعريف هذه العناصر مانحةً إياها دلالات قد تبدو مربكةً نوعًا ما، وإليك قاعدة مهمة جدًا: قد يكون استخدام العناصر <b> أو <i> أو <u> ملائمًا لإيصال معنى محدد يمكن إيصاله تقليديًا بإمالة الخط أو تثخينه أو وضع سطر تحته إذا لم توجد عناصر أخرى مناسبة، لكن عليك أن تبقي في حساباتك مفهوم سهولة الوصول أو الشمولية accessibility، فليس استخدام الخط المائل في غير سياقه مفيدًا لمستخدمِي قارئات الشاشة أو للأشخاص الذين يستخدِمون أبجديات مختلفة عن اللاتينية. <i>: يُستخدَم لإيصال معنى يمكن إيصاله تقليديًا بخط مائل مثل كلمات أجنبية، مرادفات، تصميم، مصطلحات تقنية، أفكار… <b>: يُستخدَم لإيصال معنى يمكن إيصاله تقليديًا بخط ثخين مثل كلمات مفتاحية، أسماء منتجات، جملة افتتاحية.. <u>: يُستخدَم لإيصال معنى يمكن إيصاله تقليديًا بخط تحته سطر مثل الأخطاء الإملائية والنحوية. ملاحظة: يربط الناس جدًا بين الكلمات التي تحتها سطر والروابط التشعبية، لذلك يفضَّل في صفحات الويب أن لا تضع سطرًا تحت محتوى ما لم يكن رابطًا، واستخدم <u> عندما تجد أنّ دلالته صحيحة، وفكّر دائمًا باستخدام CSS لتغيير المحتوى الذي تحته سطر إلى شكل أكثر ملاءمةً لويب، كما يشرح المثال التالي ما يمكن فعله: <!-- مصطلحات علمية --> <p> الطنان ياقوتي الحنجري (<i>Archilochus colubris</i>) هو الطنان الأكثر انتشارًا في أمريكا الشمالية. </p> <!-- كلمات أجنبية --> <p> كانت القائمة بحرًا من الكلمات الغريبة <i lang="uk-latn">vatrushka</i>, <i lang="id">nasi goreng</i> و <i lang="fr">soupe à l'oignon</i>. </p> <!-- أخطاء إملائية غير معروفة --> <p> Someday I'll learn how to <u style="text-decoration-line: underline; text-decoration-style: wavy;">spel</u> better. </p> <!-- تظليل كلمات عند عرض إرشادات --> <ol> <li> <b>شرِّح</b> قطعتين من الخبز </li> <li> <b>أدخل</b> شريحة طماطم وورقة خس بين شريحتي الخبز </li> </ol> خلاصة هذا كل شيء حتى الآن، ومن المفترض أن يكسبك هذا المقال فكرةً جيدةً عن توصيف النصوص وهيكلتها في HTML وأن يعرّفك على أهم العناصر المستخدمة لهذه الأغراض، كما هنالك الكثير من العناصر التي تحمل دلالات مختلفة وسنطَّلع على الكثير منها عند التطرق إلى التنسيق المتقدم للنصوص لاحقًا، وسنستعرض في المقال التالي تفاصيل إنشاء الروابط التشعبية في HTML والذي يُعَدّ العنصر الأهم تقريبًا في ويب. ترجمة -وبتصرف- للمقال HTML text fundamentals. اقرأ أيضًا هيكلة وتوزيع محتوى صفحات الويب HTML و CSS للمبتدئين: كيف تصمم أول صفحة ويب لك ترويسة الصفحة والبيانات الوصفية في HTML
  8. تُعَدّ الترويسة الجزء الذي لا تعرضه متصفحات ويب من الصفحة عند تحميلها، وتضم معلومات مثل عنوان الصفحة وروابط إلى ملفات تنسيق CSS إذا أردت تنسيق محتوى صفحتك، بالإضافة إلى روابط إلى أيقوناتك المفضلة وغيرها من البيانات الوصفية مثل المؤلف والكلمات المفتاحية الهامة التي تصف الصفحة، كما تستخدِم المتصفحات البيانات الموجودة في الترويسة لتصيير صفحة HTML بصورة صحيحة، وسنناقش في هذا المقال كل النقاط السابقة لتقف على ركيزة قوية عندما تعمل مع HTML. لا بد قبل متابعة القراءة أن تطلع على أساسيات HTML التي ذكرناها في مقال تعرّف على لغة HTML. ما هي ترويسة صفحة HTML لنراجع صفحة HTML البسيطة التي عرضناها في المقال السابق: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>My test page</title> </head> <body> <p>This is my page</p> </body> </html> الترويسة هي محتوى العنصر <head>، ولا تُعرض محتويات الترويسة على خلاف محتوى العنصر <body> الذي يعرضه المتصفح، لأن وظيفتها هي احتواء معلومات وصفية عن الصفحة، كما إنّ محتوى الترويسة في المثال السابق قليل كما نرى: <head> <meta charset="utf-8"> <title>My test page</title> </head> لكن في الصفحات الكبيرة قد يغدو محتوى الترويسة أكبر، وحاول فتح بعض الصفحات الكبيرة التي تفضلها ثم استخدام أدوات مطوري ويب لتطلع على محتويات الترويسة، كما أننا لا نهدف في هذا المقال إلى استعراض كل ما يمكن وضعه ضمن الترويسة، وإنما لتتعلم استخدام العناصر الرئيسية التي ستجد أن إدراجها ضمن الترويسة ضروري ولتعتاد على ذلك. لنبدأ إذًا. إضافة عنوان للصفحة رأينا سابقًا عمل العنصر <title> والذي يُستخدَم لإضافة عنوان إلى مستند HTML، لكنك قد تخلط بينه وبين العنوان الرئيسي <h1> الذي يُستخدَم في إضافة عنوان رئيسي لمحتوى الصفحة المرئي ضمن العنصر <body>، كما يُدعى العنصر <h1> أحيانًا باسم عنوان الصفحة، وتتلخص الاختلافات بما يلي: يظهر العنصر <h1> على صفحة الويب عند تحميلها ضمن المتصفح، ومن المفترض أن يُستخدَم مرةً واحدةً في الإشارة إلى العنوان الرئيسي لمحتوى الصفحة، أي عنوان قصة أو عنوان رئيسي لنشرة إخبارية وغير ذلك. يُعَدّ العنصر <title> عنصرًا وصفيًا يمثل عنوان مستند HTML برمته وليس عنوان المحتوى المرئي للصفحة. تطبيق: تفحص مثال بسيط لتبدأ العمل عليك تنزيل نسخة عن الملف "title-example.html" من المستودع المخصص على جيت-هاب بإحدى الطريقتين التاليتين: انسخ محتوى الملف ثم الصقه ضمن ملف نصي جديد باستخدام محرِّر النصوص المتوفر لديك، ثم احفظه في مكان مناسب. اضغط الزر "Raw" في صفحة جيت-هاب لتظهر الشيفرة (ربما في نافذة جديدة للمتصفح)، ثم اختر ملف File ثم حفظ الصفحة باسم Save Page As، واختر بعد ذلك مكانًا مناسبًا لتخزين الملف. افتح بعد ذلك الملف باستخدام المتصفح وسيبدو بالصورة التالية: من الواضح الآن أين سيظهر عنوان الصفحة <h1> وعنوان المستند <title>. جرب بعدها أيضًا فتح الشيفرة ضمن المحرِّر النصي وتعديل محتوى هذين العنصرين، ثم حدّث المحتوى المعروض ضمن المتصفح. يُستخدَم محتوى العنصر <title> بطرق أخرى، فإذا أردت مثلًا إضافة علامةً مرجعيةً إلى صفحة، فستجد أنّ هذا المحتوى سيكون الاسم المقترح لهذه العلامة، كما يُستخدَم في نتائج البحث كما سنرى لاحقًا. عنصر البيانات الوصفية تعتمد HTML طريقةً رسميةً في إضافة البيانات الوصفية metadata إلى المستند من خلال العنصر <meta>، كما يمكن أن تُعدَّ بقية الأمور التي نتحدث عنها في هذا المقال بيانات وصفية أيضًا، فهنالك أنواع عديدة للبيانات الوصفية تُضاف إلى جانب <meta> داخل ترويسة الصفحة <head>، لكننا لن نشرحها في هذه المرحلة لأنها ستربكك كثيرًا، وبدلًا من ذلك سنوضِّح عدة أمور قد تشاهدها كثيرًا وذلك لتأخذ فكرةً عنها. تحديد مجموعة المحارف المستخدمة في صفحتك ستجد في مثالنا السابق سطرًا يبدو كما يلي: <meta charset="utf-8"> يحدد هذا العنصر مجموعة المحارف المستخدَمة في ترميز الصفحة، وتُعَدّ المجموعة مجموعةً عالميةً من المحارف وتضم تقريبًا أيّ محرف يُستخدَم في اللغات البشرية المكتوبة، أي ستعرض صفحتك المحتوى المكتوب أيًا كانت لغته، فلهذا من الجيد دائمًا تحديد هذه المجموعة من المحارف في كل صفحة تبنيها، فقد تتعامل صفحتك مع اليابانية والإنجليزية في الوقت نفسه دون أية مشاكل تذكر: قد لا تُعرض الصفحة بالصورة الصحيحة إذا استخدمت مجموعة المحارف ISO-8859-1 مثل مجموعة المحارف الخاصة بالأبجدية اللاتينية: ملاحظة: تصحِّح بعض المتصفحات -مثل كروم- هذه الأخطاء تلقائيًا، لذلك قد لا ترى هذه المشكلة في بعض المتصفحات، ومع ذلك يُفضّل استخدام مجموعة المحارف utf-8 لتلافي أيّ مشاكل محتمَلة في المتصفحات المختلفة. تطبيق: اختبار مجموعات المحارف عُد إلى قالب HTML الذي عملنا عليه قبل قليل في فقرة "إضافة عنوان للصفحة"، وحاول تغيير قيمة السمة charset للعنصر <meta> إلى ISO-8859-1 وستدخِل المحارف اليابانية إلى صفحتك: <p>Japanese example: ご飯が熱い。</p> إضافة اسم المؤلف ووصف للصفحة قد يحتوي العنصر <meta> على سمات مثل name و content: name: يحدِّد نوع البيانات التي يعرضها العنصر <meta>. content: يحدِّد المحتوى الفعلي للعنصر. تساعدك الصفتين السابقتين على إضافة مؤلف الصفحة وتزودك بوصف مناسب لمحتواها، وإليك مثالًا كما يلي: <meta name="author" content="Chris Mills"> <meta name="description" content="The MDN Web Docs Learning Area aims to provide complete beginners to the Web with all they need to know to get started with developing web sites and applications."> يساعدك تحديد اسم المؤلف في نواح عدة منها الحصول على أجوبة عن نقاط في المحتوى أو التواصل معه، كما تتيح بعض أنظمة إدارة المحتوى إمكانية الاستخراج التلقائي لمعلومات مؤلف الصفحة لكي تستخِدمها لاحقًا للاستفسار عن المحتوى أو التواصل، ومن الجيد أيضًا أن تقدِّم وصفًا لصفحتك يضم كلمات مفتاحيةً تتعلق بطبيعة المحتوى الذي تقدمه، فقد يزيد ذلك من فرصة تصدُّر صفحتك لنتائج محركات البحث، إذ يُدعى هذا الأمر تحسين محركات البحث Search Engine Optimization أو SEO اختصارًا. تطبيق: استخدام وصف الصفحة في محركات البحث يُستخدَم وصف الصفحة أيضًا في نتائج محركات البحث، وسنوضِّح ذلك من خلال المثال التالي: انتقل إلى الصفحة الرئيسية لموقع حسوب. اعرض الشيفرة المصدرية للصفحة بالنقر عليها بالزر اليميني للفأرة ثم اختر "عرض الشيفرة المصدرية للصفحة View Page Source" من القائمة. ابحث عن البيانات الوصفية التي تحدِّد وصفًا للصفحة، وستبدو لك كما يلي: <meta name="description" content="في مهمة لتطوير العالم العربي. نعمل لنمكّن الشباب ونفتح مزيدًا من الفرص أمامهم. نحن حسوب."> ابحث الآن عن "حسوب" باستخدام محرك البحث جوجل وستلاحظ كيف استخدم عنوان المستند <title> والبيانات الوصفية <meta> في إظهار نتيجة البحث. ملاحظة: قد تلاحظ في جوجل صفحات فرعيةً مرتبطةً بنتيجة البحث مرتبةً في قائمة أسفل رابط الصفحة الرئيسية، إذ تُدعى هذه الصفحات باسم الروابط الداخلية siteLink ويمكن إدارتها من خلال الأداة Google's webmaster tools لتبدو نتائج البحث أفضل. ملاحظة: لم تعُد بعض البيانات الوصفية مستخدَمةً حاليًا مثل الكلمات المفتاحية "keywords": <meta name="keywords" content="fill, in, your, keywords, here"> التي يُفترض استخدامها من قبل محركات البحث للحكم على مطابقة صفحة ما لمعايير البحث، وتتجاهل محركات البحث حاليًا هذه الكلمات، إذ يضيف بعض المخترقين مئات من هذه الكلمات للحصول على نتائج منحازة إلى الصفحات التي يريدونها. أنواع أخرى من البيانات الوصفية قد تصادف في رحلتك ضمن عالم ويب أنواع أخرى من البيانات الوصفية، والكثير من الميزات التي ستراها في مواقع الويب هي ملكية إبداعية خاصة صُمِّمت لتزويد بعض المواقع بمعلومات محددة مثل مواقع التواصل الاجتماعي، فقد طوّرت الفيسبوك مثلًا بروتوكول بيانات وصفية يُدعى Open Graph Data لكي يقدّم بيانات وصفيةً غنيةً عن موقع ما، فإذا اطلعت على الشيفرة المصدرية لموقع "MDN Web Docs"، فستجد التالي: <meta property="og:image" content="https://developer.mozilla.org/static/img/opengraph-logo.png"> <meta property="og:description" content="The Mozilla Developer Network (MDN) provides information about Open Web technologies including HTML, CSS, and APIs for both Web sites and HTML5 Apps. It also documents Mozilla products, like Firefox OS."> <meta property="og:title" content="Mozilla Developer Network"> سيظهر الرابط مزوّدًا بصورة ووصف للموقع عندما تضع رابطًا إلى موقع "MDN Web Docs" على فيسبوك، مما يزيد غنى تجربة المستخدِم: يستخدِم تويتر بروتوكولًا وصفيًا مماثلًا يُدعى Twitter Cards يقدِّم محتوًى غنيًا عن عنوان URL محدد عندما يُعرض على twitter.com، وإليك مثالًا كما يلي: <meta name="twitter:title" content="Mozilla Developer Network"> إضافة أيقونات مخصصة إلى صفحتك تستطيع الإشارة إلى أيقونات مخصصة تُعرض في مناسبات محددة من خلال البيانات الوصفية لكي تُغني تصميم صفحتك، وأكثر هذه الأيقونات استخدامًا هي الأيقونة المفضلة favicon والتي تُستخدم لتدل على صفحتك عندما تنشئ علامةً مرجعيةً إليها في متصفحك أو عندما تضيفها إلى المفضلة، ولا يزال استخدام هذه الأيقونة شائعًا منذ سنوات عديدة، فهي عبارة عن أيقونة مربعة بعرض 16 بكسل تشاهدها في أماكن عدة مثل عناوين نوافذ المتصفح وإلى جانب العلامات المرجعية للصفحات، كما يمكنك إضافة الأيقونة المفضلة على صفحتك كما يلي: احفظ الأيقونة المطلوبة باللاحقة ico. في المجلد نفسه الذي يحتوي على الصفحة index.html، وقد تدعم بعض المتصفحات لواحق أخرى مثل gif. أو png.، لكن يضمن لك استخدام اللاحقة السابقة العمل على أيّ متصفح رجوعًا إلى إنترنت أكسبلورر 6. أضف السطر التالي إلى ترويسة ملف HTML: <link rel="icon" href="favicon.ico" type="image/x-icon"> إليك مثالًا عن أيقونة مفضلة إلى جانب علامة مرجعية: ستجد أيضًا الكثير من أنواع الأيقونات حاليًا، فإذا فتحت الشيفرة المصدرية لموقع "MDN Web Docs"، فستجد التالي: <!-- third-generation iPad with high-resolution Retina display: --> <link rel="apple-touch-icon-precomposed" sizes="144x144" href="https://developer.mozilla.org/static/img/favicon144.png"> <!-- iPhone with high-resolution Retina display: --> <link rel="apple-touch-icon-precomposed" sizes="114x114" href="https://developer.mozilla.org/static/img/favicon114.png"> <!-- first- and second-generation iPad: --> <link rel="apple-touch-icon-precomposed" sizes="72x72" href="https://developer.mozilla.org/static/img/favicon72.png"> <!-- non-Retina iPhone, iPod Touch, and Android 2.1+ devices: --> <link rel="apple-touch-icon-precomposed" href="https://developer.mozilla.org/static/img/favicon57.png"> <!-- basic favicon --> <link rel="icon" href="https://developer.mozilla.org/static/img/favicon32.png"> تستخدَم الأيقونات السابقة لإظهار أيقونات عالية الدقة عندما يُحفظ موقع ويب على الشاشة الرئيسية لجهاز آيباد. لا تقلق حيال هذا الكم الكبير من الأيقونات وضرورة إدراجها الآن، فهي ميزة متقدِّمة نوعًا ما ولا حاجة لأن تعرف عنها الكثير حتى تكمل معنا المسيرة خلال هذه المقالات، فالغاية الحقيقية من هذا العرض هو إطلاعك على أمور مثل هذه قد تصادفها أثناء تصفحك للشيفرة المصدرية للمواقع. ملاحظة: في حال استخدمت سياسة خصوصية المحتوى Content Security Policy أو CSP اختصارًا في موقعك، فستُطبق هذه السياسة على أيقونة المفضلة favico، فإذا لم يُحمّل المتصفح هذه الأيقونة، فتأكد من أنّ التوجيه img-src العائد للترويسة Content-Security-Policy لا يعيق الوصول إلى الأيقونة. تطبيق تنسيقات CSS وشيفرة جافاسكربت على لغة HTML تستخدِم معظم المواقع في أيامنا هذه لغة CSS لتحسين مظهر الصفحة وجافاسكربت لإكساب الصفحة قدرات تفاعلية مع المستخدِم مثل مشغلات الفيديو والخرائط والألعاب وغيرها، إذ تُطبَّق هذه الأمور على الصفحات باستخدام العنصر <link> والعنصر <script>: <link>: ينبغي وضع هذا العنصر ضمن ترويسة الملف <head> ويمتلك صفتين هما "rel="stylesheet والتي تشير إلى أنّ الرابط سيستخدَم لتنسيق الصفحة، والسمة href التي تحدد مسارًا إلى ملف التنسيق: <link rel="stylesheet" href="my-css-file.css"> <script>: ينبغي أن يكون أيضًا ضمن الترويسة، كما ينبغي أن يمتلك السمة src التي تحتوي على مسار ملف جافاسكربت الذي تريد استخدامه، بالإضافة إلى السمة defer التي تدفع بالمتصفح إلى تحميل ملف الشيفرة بعد الانتهاء من تفسير شيفرة لغة HTML، وللأمر أهميته في ضمان تحميل كل عناصر لغة HTML قبل تشغيل شيفرة جافاسكربت، وبالتالي لن تقع أخطاء نتيجة محاولة جافاسكربت الوصول إلى عناصر لغة HTML غير موجودة بعد في الصفحة، وهناك طرق عدة للتحكم بتحميل شيفرة جافاسكربت ضمن الصفحات، لكنه الأسلوب الأكثر فعاليةً واستخدامًا في المتصفحات الحديثة: <script src="my-js-file.js" defer></script> ملاحظة: قد يبدو لك العنصر <script> فارغًا لكنه ليس كذلك، كما ينبغي استخدام وسم النهاية، إذ نستطيع أيضًا كتابة الشيفرة داخل هذا العنصر بدلًا من الإشارة إلى ملف خارجي. تطبيق: استخدام لغة CSS ولغة جافاسكربت في صفحتنا لبدء العمل على هذا التطبيق، أحضر نسخًا عن الملفات meta-example.html و script.js و style.css وخزِّنها على حاسوبك في المجلد نفسه، ثم تأكد من حفظها تمامًا بالأسماء واللواحق -أي الامتدادات- السابقة. افتح ملف HTML في المتصفح وفي محرِّر النصوص معًا. أضف العنصر <link> إلى شيفرة HTML لكي تشير إلى ملفَي CSS وجافاسكربت كلًا على حدة كما فعلنا سابقًا. إذا نجحت في تنفيذ الأمر، فسترى أنّ الأمور قد تغيرت ليعرض المتصفح بعد تحديثه ما يلي: أضافت شيفرة جافاسكربت قائمةً فارغةً إلى الصفحة، فإذا نقرت الآن في أيّ مكان من الصفحة، فستظهر لك رسالةً منبثقةً تسألك إضافة بعض الكلمات لتكوين عنصر قائمة جديد، وعند النقر على الزر "موافق OK"، سيُضاف عنصر جديد إلى القائمة يحمل النص الذي أدخلته؛ أما عندما تنقر على عنصر قائمة موجود، فستظهر لك رسالة منبثقة لتغيير محتوى هذا العنصر. غيّرت CSS الخلفية إلى اللون الأخضر وكبّرت حجم النص، كما غيّرت في تنسيق بعض العناصر التي أضافتها جافاسكربت، أي الشريط الأحمر والإطار الأسود للقائمة التي ولّدتها جافاسكربت. ملاحظة: لو وجدت نفسك تائهًا في هذا التمرين ولم تتمكن من إحضار ملفات CSS وجافاسكربت، فحاول التحقق من الملف css-and-js.html. ضبط اللغة الرئيسية للصفحة لابد أخيرًا من الإشارة إلى إمكانية وضرورة تحديد لغة الصفحة، فيمكن إنجاز الأمر من خلال السمة lang للعنصر الجذري <html>: <html lang="en-US"> للأمر فوائد عدة منها فهرسة موقعك بفعالية من قِبَل محركات البحث، إذ يسمح ذلك بظهور الموقع في النتائج المرتبطة بهذه اللغة مثلًا، كما يساعد ذلك كثيرًا الأشخاص ذوي الإعاقات البصرية الذين يستخدِمون قارئات الشاشة، فالكلمة "six" مثلًا موجودة في الفرنسية والإنكليزية لكنها تلفظ بصورة مختلفة، كما يمكنك أيضًا تحديد أجزاء من صفحتك لتعرض بلغة مختلفة، إذ يمكنك مثلًا اختيار اللغة لقسم فقط من الصفحة كما يلي: <p>Japanese example: <span lang="ja">ご飯が熱い。</span>.</p> ملاحظة: تُحدَّد رموز اللغات بواسطة المعيار ISO 639-1. خلاصة بهذا نكون قد وصلنا إلى نهاية هذا العرض التمهيدي لترويسة HTML، وهنالك الكثير مما يمكن إنجازه أيضًا لكن ستكون الإطالة في الأمر مزعجةً ومربكةً في هذه المرحلة، فكل ما نريده هو إيصال الأفكار الأكثر شيوعًا حول الموضوع، وسنلقي نظرةً في المقال القادم على أساسيات نصوص HTML. ترجمة -وبتصرف- للمقال What’s in the head? Metadata in HTML. اقرأ أيضًا HTML و CSS للمبتدئين: كيف تصمم أول صفحة ويب لك HTML و CSS للمبتدئين: مقدمة إلى تنسيقات CSS خمسة أشياء عليك معرفتها عن HTML5 مدخل إلى البيانات الوصفية (microdata) في HTML5
  9. نستكشف في هذا المقال تطبيقات نود Node التي تستخدم قواعد بيانات علاقية، وسنبني خلال تقدمنا في المقال واجهةً خلفيةً تستخدم قاعدة بيانات علاقية ليتعامل معها تطبيق الملاحظات الذي عملنا عليه سابقًا. ولإكمال هذا المقال، لا بُدّ من تعميق معرفتك بقواعد البيانات العلاقية ولغة SQL. يمكنك الاطلاع على سلسلة مقالات المرجع المتقدم إلى SQL في أكاديمية حسوب، وكذلك الاطلاع على توثيق لغة SQL في موسوعة حسوب. ستجد 24 تمرينًا في هذا المقال وعليك أن تنجزها جميعًا لتكمل الدورة التعليمية، تُسلّم الحلول إلى منظومة تسليم الحلول كما هو الحال في الأقسام السابقة ما عدا الأقسام من 0 إلى 7 والتي تُسلّم حلول التمارين فيها إلى مكان آخر. إيجابيات وسلبيات قواعد بيانات المستندات استخدمنا قاعدة البيانات MongoDB في جميع المقال السابقة، وهي قاعدة بيانات مستندات، ومن أهم ميزاتها أنها لا تملك أي مخططات، أي أنّ لها معرفةٌ محدودةٌ بطبيعة البيانات المخزنة في كل مجموعة. يتواجد مخطط قاعدة البيانات schema في شيفرة البرنامج فقط، إذ يفسِّر البيانات بطريقة معينة، كأن يحدد أن بعض الحقول هي مراجعٌ لكائنات في مجموعةٍ أخرى. رأينا في المثال التطبيقي الذي يضم قاعدة بيانات تخزن الملاحظات والمستخدمين (في القسمين 3 و 4)، إذ تُخزِّن الملاحظات "notes" على النحو التالي: [ { "_id": "600c0e410d10256466898a6c", "content": "HTML is easy" "date": 2021-01-23T11:53:37.292+00:00, "important": false "__v": 0 }, { "_id": "600c0edde86c7264ace9bb78", "content": "CSS is hard" "date": 2021-01-23T11:56:13.912+00:00, "important": true "__v": 0 }, ] وتُخزّن أيضًا بيانات المستخدمين في المجموعة "users" على النحو التالي: [ { "_id": "600c0e410d10256466883a6a", "username": "mluukkai", "name": "Matti Luukkainen", "passwordHash" : "$2b$10$Df1yYJRiQuu3Sr4tUrk.SerVz1JKtBHlBOARfY0PBn/Uo7qr8Ocou", "__v": 9, notes: [ "600c0edde86c7264ace9bb78", "600c0e410d10256466898a6c" ] }, ] تعرِف MongoDB نوع البيانات في الحقول التي تُخزِّن كيانات البيانات، لكنها لا تعرف إلى أي مجموعة من الكيانات يشير المعرِّف المميز الذي سجّله المستخدم، ولا تهتم أيضًا بالحقول التي تمتلكها الكيانات المُخزنة في مجموعات البيانات؛ لهذا تترك MongoDB أمر التحقق من صحة المعلومات المخزّنة في قاعدة البيانات إلى المبرمج. هناك طبعًا إيجابيات وسلبيات لعدم وجود مخطط لقاعدة البيانات، وإحدى الإيجابيات هي المرونة التي تضيفها الميزة اللا إدارية في مخطط البيانات، إذ سيسرّع انتفاء الحاجة إلى تعريف مخطط على مستوى قاعدة البيانات من عمل المطوّر في حالات عدة، كما أنه أسهل من تعريف وتعديل المخطط في حالات أخرى؛ في حين تتعلق المشاكل الناجمة عن عدم وجود المخطط غالبًا بسهولة الوقوع في الأخطاء، فكل شيء ملقىً على عاتق المبرمج، وليس لقاعدة البيانات في هذه الحالة طريقةً للتحقق من نزاهتها honest، أي إذا احتوت كل الحقول الإجبارية على القيم الصحيحة، أو إذا أشارت الحقول ذات النوع المرجعي إلى كيانات من النوع الصحيح عمومًا وهكذا. تعتمد قاعدة البيانات العلاقية التي ستكون محور حديثنا في هذا المقال بشدة على وجود مخطط، وتكاد تكون الإيجابيات والسلبيات في استخدامها على نقيض تلك المتعلقة بقاعدة بيانات المستندات. يعود السبب الرئيسي في استخدام قاعدة البيانات MongoDB تحديدًا في الأقسام السابقة إلى طبيعتها التي لا تحتاج مخططات، الأمر الذي يسهّل استخدامها بالنسبة لذوي المعرفة المحدودة بقواعد البيانات العلاقية، لكن نفترض بالنسبة لكل الحالات التي عرضناها في منهاجنا أن القواعد العلاقية هي الأنسب. قاعدة بيانات التطبيق نحتاج في تطبيقنا إلى قاعدة بيانات علاقية، وهناك خيارات عديدة، لكننا سنستخدم حاليًا الحل الأكثر شعبيةً والمفتوح المصدر PostgreSQL؛ ويمكنك تثبيت Postgres (هكذا تُدعى قاعدة البيانات هذه غالبًا) على جهازك إذا أردت؛ والأسهل من هذا هو استخدامها بمثابة خدمة سحابية، مثل ElephantSQL. بإمكانك أيضًا الاستفادة مما جاء في جزئية سابقة من السلسلة لاستخدام Postgres محليًا من خلال دوكر Docker. اخترنا في هذا المقال الاستفادة من إمكانية إنشاء قاعدة بيانات Postgres للتطبيق على خدمة "Heroku" السحابية التي ألفنا العمل معها في المقالين 3 و4. سنبني في الجزء النظري من هذا المقال نسخةً مرتبطة بقاعدة البيانات Postgres عن الواجهة الخلفية لتطبيق تخزين الملاحظات الذي بنيناه في المقالين 3 و4. لننشئ أولًا مجلدًا مناسبًا ضمن تطبيق Heroku ونضيف إليه قاعدة بيانات، ثم نستخدم الأمر heroku config للحصول على "السلسلة النصية connect string" اللازمة للاتصال مع القاعدة: heroku create # heroku يعيد اسم التطبيق للتطبيق الذي أنشأته في heroku addons:create heroku-postgresql:hobby-dev -a <app-name> heroku config -a <app-name> === cryptic-everglades-76708 Config Vars DATABASE_URL: postgres://<username>:<password>@<host-of-postgres-addon>:5432/<db-name> يُعد الوصول إلى قاعدة البيانات مباشرةً من الأمور الأساسية التي ينبغي الانتباه إليها تحديدًا في قواعد البيانات العلاقية، نظرًا لوجود أساليب عدة لتنفيذ الأمر، ووجود عدة واجهات مستخدم رسومية، مثل pgAdmin، لكن سنستخدم أداة سطر الأوامر psql الخاصة بالقاعدة Postgres. يمكن الوصول إلى قاعدة البيانات من خلال تنفيذ أمر psql على خادم Heroku على النحو التالي (لاحظ كيف تعتمد معاملات الأمر على عنوان url للاتصال بقاعدة بيانات Heroku): heroku run psql -h <host-of-postgres-addon> -p 5432 -U <username> <dbname> -a <app-name> بعد إدخال كلمة المرور، سننفذ أمر psql الأساسي وهو d\، الذي يخبرك بمحتوى قاعدة البيانات: Password for user <username>: psql (13.4 (Ubuntu 13.4-1.pgdg20.04+1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) Type "help" for help. username=> \d Did not find any relations. وكما ترى، لا يوجد شيء حاليًا في قاعدة البيانات. لننشئ إذًا جدولًا للملاحظات: CREATE TABLE notes ( id SERIAL PRIMARY KEY, content text NOT NULL, important boolean, date time ); يمكننا ملاحظة بعض النقاط: يُعرِّف العمود id مفتاحًا أساسيًا primary key، ويعني ذلك أن قيم هذا العمود لا بُدّ أن تكون فريدةً لكل سطر في الجدول ولا ينبغي أن تكون قيمتها فارغة. يُعرّف نوع العمود id على أنه تسلسلي SERIAL، وهو ليس نوع حقيقي بل تمثيل لعمود ذي قيم صحيحة تعيّن Postgres قيمه الفريدة تلقائيًا وتزيد هذه القيمة بمقدار "واحد" عند إنشاء سطر جديد. يكون العمود المُسمى content من النوع النصي ويُعُرِّف كي تعيّن له قيمةٌ في كل سطر. لنلق نظرةً على الوضع الآن من خلال الطرفية، ولننفّذ أولًا الأمر d\ الذي يعرض جداول قاعدة البيانات: username=> \d List of relations Schema | Name | Type | Owner --------+--------------+----------+---------------- public | notes | table | username public | notes_id_seq | sequence | username (2 rows) إذ تُنشئ Postgres إضافةً إلى الجدول notes جدولًا فرعيًا يُدعى notes_id_seq يتتبع القيم المُسندة إلى العمود id عند إنشاء ملاحظة جديدة. وبتنفيذ الأمر d notes\ يمكننا رؤية طريقة تعريف الجدول notes: username=> \d notes; Table "public.notes" Column | Type | Collation | Nullable | Default -----------+------------------------+-----------+----------+----------------------------------- id | integer | not null | nextval('notes_id_seq'::regclass) content | text | | not null | important | boolean | | | | date | time without time zone | | | | Indexes: "notes_pkey" PRIMARY KEY, btree (id) إذًا، للعمود id قيم افتراضية تُستخرج من تنفيذ الدالة الداخلية nextval في Postgres. لنُضِف بعض المحتوى إلى الجدول: insert into notes (content, important) values ('Relational databases rule the world', true); insert into notes (content, important) values ('MongoDB is webscale', false); لنرى الآن كيف يبدو المحتوى الذي أضفناه: username=> select * from notes; id | content | important | date ----+-------------------------------------+-----------+------ 1 | relational databases rule the world | t | 2 | MongoDB is webscale | f | (2 rows) إذا حاولنا تخزين البيانات في قاعدة البيانات دون العودة إلى مخطط، فلن ينجح الأمر، لأنّ قيم الأعمدة الإجبارية لا بُد أن تكون موجودةً: username=> insert into notes (important) values (true); ERROR: null value in column "content" of relation "notes" violates not-null constraint DETAIL: Failing row contains (9, null, t, null). ولا يمكن أن تكون قيمة العمود من النوع الخاطئ: username=> insert into notes (content, important) values ('only valid data can be saved', 1); ERROR: column "important" is of type boolean but expression is of type integer LINE 1: ...tent, important) values ('only valid data can be saved', 1); ^ ولا يمكن القبول بأعمدة غير موجودة في المخطط: username=> insert into notes (content, important, value) values ('only valid data can be saved', true, 10); ERROR: column "value" of relation "notes" does not exist LINE 1: insert into notes (content, important, value) values ('only ... سننتقل تاليًا إلى طريقة الدخول إلى قاعدة البيانات من التطبيق. تطبيق Node يستخدم قاعدة بيانات علاقية لنشغّل التطبيق كما جرت العادة من خلال التعليمة npm init ونثبّت "nodemon" على أنه اعتمادية تطوير development dependency وكذلك اعتماديات زمن التشغيل التالية: npm install express dotenv pg sequelize نجد من بين هذه الاعتماديات sequelize، وهي المكتبة التي يمكننا من خلالها استخدام Postgres؛ وتنتمي هذه المكتبة إلى مكتبات الربط العلاقي للكائنات Object relational mapping -أو اختصارًا ORM-، التي تسمح بتخزين كائنات جافا سكربت JavaScript في قاعدة بيانات علاقية دون استخدام لغة SQL بحد ذاتها وبصورةٍ مشابهة للمكتبة "Mongoose" التي استخدمناها مع قاعدة البيانات MongoDB. لنختبر الآن قدرتنا على الاتصال الناجح بقاعدة البيانات، لهذا أنشئ الملف index.js وأضف إليه الشيفرة التالية: require('dotenv').config() const { Sequelize } = require('sequelize') const sequelize = new Sequelize(process.env.DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }) const main = async () => { try { await sequelize.authenticate() console.log('Connection has been established successfully.') sequelize.close() } catch (error) { console.error('Unable to connect to the database:', error) } } main() ينبغي تخزين سلسلة الاتصال النصية connect string بقاعدة البيانات التي أظهرها الأمر heroku config في ملفٍ له الامتداد env.، وينبغي أن تكون مشابهةً لما يلي: $ cat .env DATABASE_URL=postgres://<username>:<password>@ec2-54-83-137-206.compute-1.amazonaws.com:5432/<databasename> لنتحقق من نجاح الاتصال: $ node index.js Executing (default): SELECT 1+1 AS result Connection has been established successfully. إذا عمل الاتصال، يمكننا حينها تشغيل الاستعلام الأول. لنعدّل البرنامج على النحو التالي: require('dotenv').config() const { Sequelize, QueryTypes } = require('sequelize') const sequelize = new Sequelize(process.env.DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); const main = async () => { try { await sequelize.authenticate() const notes = await sequelize.query("SELECT * FROM notes", { type: QueryTypes.SELECT }) console.log(notes) sequelize.close() } catch (error) { console.error('Unable to connect to the database:', error) } } main() ينبغي أن يطبع تنفيذ البرنامج ما يلي: Executing (default): SELECT * FROM notes [ { id: 1, content: 'Relational databases rule the world', important: true, date: null }, { id: 2, content: 'MongoDB is webscale', important: false, date: null } ] وعلى الرغم من كون Sequelize مكتبة ربط علاقي للكائنات ORM، أي أنها تحتاج إلى قليلٍ فقط من شيفرة SQL التي تكتبها بنفسك، فقد استخدمنا شيفرة SQL مباشرةً مع تابع Sequelize الذي يُدعى query. طالما أنّ كل شيء يعمل على ما يرام، لنحوّل التطبيق إلى تطبيق ويب. require('dotenv').config() const { Sequelize, QueryTypes } = require('sequelize') const express = require('express') const app = express() const sequelize = new Sequelize(process.env.DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); app.get('/api/notes', async (req, res) => { const notes = await sequelize.query("SELECT * FROM notes", { type: QueryTypes.SELECT }) res.json(notes) }) const PORT = process.env.PORT || 3001 app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) }) يبدو أن التطبيق يعمل جيدًا. لنتحول الآن إلى استخدام Sequelize بدلًا من SQL. النماذج في Sequelize يُمثَّل كل جدول من جداول قاعدة البيانات عند استخدام Sequelize بنموذج model، والذي يُعدُّ صنف JavaScript الخاص بالجدول. لنعرِّف الآن النموذج Note المتعلق بالجدول notes من التطبيق وذلك بتغيير الشيفرة على النحو التالي: require('dotenv').config() const { Sequelize, Model, DataTypes } = require('sequelize') const express = require('express') const app = express() const sequelize = new Sequelize(process.env.DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); class Note extends Model {} Note.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE } }, { sequelize, underscored: true, timestamps: false, modelName: 'note' }) app.get('/api/notes', async (req, res) => { const notes = await Note.findAll() res.json(notes) }) const PORT = process.env.PORT || 3001 app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) }) لا يوجد شيء جديد في تعريف النموذج، فكل عمودٍ له نوع معرَّف، إضافةً إلى خاصيات أخرى عند الضرورة كأن يكون العمود مفتاحًا أساسيًا للجدول. يضم المعامل الثاني في تعريف النموذج السمة sequelize إضافةً إلى غيرها من معلومات التهيئة. كما عرّفنا أن الجدول لا ينبغي أن يضم أعمدة لها بصمات زمنية timestamps، مثل created_atأنشئ عند و updated_at حُدِّث عند. عرّفنا أيضًا القيمة true إلى underscored، والتي تعني أن أسماء الجدول مشتقةٌ من أسماء النموذج لكن بصيغة الجمع وباستخدام تنسيق الأفعى snake_case، الذي تُوضع فيه الشرطة السفلية بدلًا من الفراغات بين الكلمات وتُكتب بأحرف صغيرة؛ ويعني هذا عمليًا أنه إذا كان اسم النموذج "Note"، فسيكون اسم الجدول المقابل "notes". ولو كان اسم النموذج مكونًا من جزئين مثل "StudyGroup"، فسيكون اسم الجدول المقابل "study_groups". وبدلًا من الاستدلال على أسماء الجداول تلقائيًا، تسمح لك المكتبة Sequelize بتعريف أسماء الجداول صراحةً أيضًا. تُطبق سياسة التسمية هذه على أسماء الأعمدة أيضًا. فلو عرّفنا أن ملاحظةً ما سترتبط بعام الإنشاء creationYear أي بالمعلومات المتعلقة بعام إنشاء الملاحظة، سنعرّفها في النموذج على النحو التالي: Note.init({ // ... creationYear: { type: DataTypes.INTEGER, }, }) سيكون اسم العمود المقابل في قاعدة البيانات creation_year، لكن الإشارة إلى العمود في الشيفرة تكون دائمًا باستخدام تنسيق النموذج نفسه أي طريقة سنام الجمل camel case. وعرّفنا كذلك السمة modelName التي أُسندت إليها القيمة note، وستكون القيمة الافتراضية لاسم النموذج بحرف بداية كبير "Note"، وسترى أنّ هذا أكثر ملاءمةً لاحقًا. يسهل التعامل مع قاعدة البيانات من خلال واجهة الاستعلام التي يؤمنها النموذج، إذ يعمل التابع works تمامًا كما يوحي اسمه: app.get('/api/notes', async (req, res) => { const notes = await Note.findAll() res.json(notes) }) تخبرك الطرفية أن التابع ()Note.findAll يُنفِّذ الاستعلام التالي: Executing (default): SELECT "id", "content", "important", "date" FROM "notes" AS "note"; سننجز تاليًا وصلة endpoint بغرض إنشاء ملاحظات جديدة: app.use(express.json()) // ... app.post('/api/notes', async (req, res) => { console.log(req.body) const note = await Note.create(req.body) res.json(note) }) تُضاف الملاحظة الجديدة باستدعاء التابع create الذي يوفّره النموذج "Note" بعد تمرير كائن يعرِّف قيم الأعمدة لسطر الملاحظة الجديدة على أنه وسيطٌ لهذا التابع. وبإمكانك أيضًا حفظ قاعدة البيانات بتنفيذ التابع build أولًا لإنشاء كائن نموذج من البيانات المطلوبة، ثم استدعاء التابع save وذلك بدلًا من التابع create: const note = Note.build(req.body) await note.save() لا يؤدي استدعاء التابع build إلى حفظ الكائن في قاعدة البيانات، وبالتالي من الممكن تعديله قبل تنفيذ أمر الحفظ الفعلي: const note = Note.build(req.body) note.important = true await note.save() يُعد استخدام التابع create في المثال السابق أكثر ملاءمةً لما نريده من التطبيق، لذلك سنلتزم باستخدامه من الآن فصاعدًا. إذا لم يكن الكائن المُنشأ صالحًا، ستظهر رسالة خطأ، لذلك إذا حاولت إضافة ملاحظة جديدة دون محتوى، ستخفق العملية وتكشف لك الطرفية عن السبب بأن "المحتوى لا يمكن أن يكون فارغًا": (node:39109) UnhandledPromiseRejectionWarning: SequelizeValidationError: notNull Violation: Note.content cannot be null at InstanceValidator._validate (/Users/mluukkai/opetus/fs-psql/node_modules/sequelize/lib/instance-validator.js:78:13) at processTicksAndRejections (internal/process/task_queues.js:93:5) لنضف آليةً بسيطةً لاصطياد الأخطاء عند إضافة ملاحظةٍ جديدة: app.post('/api/notes', async (req, res) => { try { const note = await Note.create(req.body) return res.json(note) } catch(error) { return res.status(400).json({ error }) } }) التمرينات 13.1 إلى 13.3 سنبني في هذه التمرينات واجهة خلفية لتطبيق مدوّنات يشابه ما فعلنا في القسم 4، وينبغي أن يتوافق مع الواجهة الأمامية التي أنشأناها في القسم 5 باستثناء معالجة الأخطاء. سنضيف أيضًا ميزات مختلفة إلى الواجهة الخلفية لا يمكن للواجهة الأمامية في القسم 5 التعامل معها. التمرين13.1 أنشئ مستودع غيت هاب GitHub مخصص للتطبيق، ثم أنشئ تطبيق Heroku خاص به إضافةً إلى قاعدة بيانات Postgres، وتأكد من قدرتك على تأسيس اتصال بين التطبيق وقاعدة البيانات. التمرين 13.2 أنشئ باستخدام سطر الأوامر الجدول "blogs" الذي يضم الأعمدة التالية: id: يمثل قيمةً فريدةً تزداد باستمرار. author: قيمة نصية. url: قيمة نصية لا يمكن أن تكون فارغة. title: قيمة نصية لا يمكن أن تكون فارغة. likes: قيمة صحيحة تبدأ من الصفر افتراضيًا. أضف مدونتين على الأقل إلى قاعدة البيانات. احفظ بعد ذلك الأوامر التي استخدمتها في ملف يُدعى "commands.sql" في جذر التطبيق. التمرين 13.3 أضف إلى تطبيقك وظيفة طباعة المدوّنات الموجودة في قاعدة البيانات باستخدام سطر الأوامر كما في المثال التالي: $ node cli.js Executing (default): SELECT * FROM blogs Dan Abramov: 'On let vs const', 0 likes Laurenz Albe: 'Gaps in sequences in PostgreSQL', 0 likes إنشاء جداول قاعدة البيانات تلقائيا يوجد في تطبيقنا الحالي جانب غير مرغوب فهو يفترض وجود قاعدة بيانات مع المخطط المعين، أي أنّ الجدول "notes" قد أُنشأ بتنفيذ الأمر create table. تُخزّن شيفرة البرنامج على غيت هاب GitHub، لذلك من المنطقي تخزين الأوامر التي أنشأت بها قاعدة البيانات ضمن سياق الشيفرة لكي يبقى مخطط قاعدة البيانات نفسه كما تتوقعه شيفرة البرنامج. يُمكن للمكتبة أن تولّد تلقائيًا مخططًا انطلاقًا من تعريف النموذج من خلال التابع sync. لندمر الآن قاعدة البيانات الموجودة من خلال أوامر الطرفية على النحو التالي: drop table notes; تأكد من تدمير القاعدة بتنفيذ الأمر d\: username=> \d Did not find any relations. لن يعمل التطبيق الآن، لهذا سننفِّذ الأمر التالي مباشرةً بعد تعريف النموذج "Note": Note.sync() عندما يعمل التطبيق سيظهر ما يلي على شاشة الطرفية: Executing (default): CREATE TABLE IF NOT EXISTS "notes" ("id" SERIAL , "content" TEXT NOT NULL, "important" BOOLEAN, "date" TIMESTAMP WITH TIME ZONE, PRIMARY KEY ("id")); وهكذا، عندما يعمل التطبيق، تُنفَّذ التعليمة: CREATE TABLE IF NOT EXISTS "notes"... التي تُنشئ الجدول "notes" إن لم يكن موجودًا. خيارات أخرى لنكمل تطبيقنا بإضافة عدة خيارات أخرى. بإمكاننا البحث عن ملاحظة محددة باستخدام التابع findByPk لأنه يبحث ضمن قيم id التي تمثّل المفتاح الرئيسي لقاعدة البيانات: app.get('/api/notes/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { res.json(note) } else { res.status(404).end() } }) يؤدي البحث عن ملاحظة محددة إلى تنفيذ أمر SQL التالي: Executing (default): SELECT "id", "content", "important", "date" FROM "notes" AS "note" WHERE "note". "id" = '1'; في حال عدم وجود أية ملاحظات، سيعيد البحث القيمة null (لا شيء)، وسيعطي رمز الحالة المناسب. تُعدَّل الملاحظة على النحو التالي، ولا يمكن تعديل سوى الحقل important لأن الواجهة الأمامية لا تحتاج أي شيء آخر: app.put('/api/notes/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { note.important = req.body.important await note.save() res.json(note) } else { res.status(404).end() } }) يُستخرج الكائن المتعلق بسطر قاعدة البيانات باستخدام التابع findByPk، ويُعدّل بعدها الكائن وتُخزَّن النتيجة باستدعاء التابع save. بإمكانك إيجاد شيفرة التطبيق كاملةً في المستودع المخصص على GitHub ضمن الفرع "part13-1". طباعة الكائن الذي تعيده Sequelize على الطرفية تُعد الأداة console.log أفضل أدوات مبرمجي JavaScript إذ يمكّنهم استعمالها المتكرر من التقاط أسوأ الثغرات. لهذا سنطبع الملاحظات على الطرفية: app.get('/api/notes/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { console.log(note) res.json(note) } else { res.status(404).end() } }) لاحظ أنّ النتيجة النهائية ليست ما نتوقعه تمامًا: note { dataValues: { id: 1, content: 'Notes are attached to a user', important: true, date: 2021-10-03T15:00:24.582Z, }, _previousDataValues: { id: 1, content: 'Notes are attached to a user', important: true, date: 2021-10-03T15:00:24.582Z, }, _changed: Set(0) {}, _options: { isNewRecord: false, _schema: null, _schemaDelimiter: '', raw: true, attributes: [ 'id', 'content', 'important', 'date' ] }, isNewRecord: false } فكل الأشياء التي يحتويها الكائن إضافةً إلى البيانات قد طُبعت على الطرفية، لهذا يمكن الوصول إلى النتيجة المرجوة باستدعاء التابع toJSON العائد إلى كائن النموذج: app.get('/api/notes/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { console.log(note.toJSON()) res.json(note) } else { res.status(404).end() } }) ها هي النتيجة الآن كما هو متوقع. { id: 1, content: 'MongoDB is webscale', important: false, date: 2021-10-09T13:52:58.693Z } في الحالة التي نريد فيها طباعة مجموعة من الكائنات، لن يعمل التابع toJSON مباشرةً، بل يجب أن يُستدعى بصورةٍ مستقلة لكل كائن في المجموعة: router.get('/', async (req, res) => { const notes = await Note.findAll() console.log(notes.map(n=>n.toJSON())) res.json(notes) }) وتبدو النتيجة على النحو التالي: [ { id: 1, content: 'MongoDB is webscale', important: false, date: 2021-10-09T13:52:58.693Z }, { id: 2, content: 'Relational databases rule the world', important: true, date: 2021-10-09T13:53:10.710Z } ] وربما من الأفضل أن تحوّل المجموعة إلى تنسيق JSON لطباعتها باستخدام التابع JSON.stringify: router.get('/', async (req, res) => { const notes = await Note.findAll() console.log(JSON.stringify(notes)) res.json(notes) }) تُعد هذه الطريقة أكثر ملاءمةً خاصةً إذا احتوت الكائنات على كائنات أخرى، كما أنها مفيدةٌ لتنسيق الكائنات على الشاشة بطريقة تُسهِّل القراءة، ويُنفَّذ ذلك من خلال الأمر التالي: console.log(JSON.stringify(notes, null, 2)) وتبدو النتيجة على النحو التالي: [ { "id": 1, "content": "MongoDB is webscale", "important": false, "date": "2021-10-09T13:52:58.693Z" }, { "id": 2, "content": "Relational databases rule the world", "important": true, "date": "2021-10-09T13:53:10.710Z" } ] التمرين 13.4 حوّل تطبيقك إلى تطبيق ويب يدعم العمليات التالية: الحصول على كل المدونات: GET api/blogs. إضافة مدوّنة جديدة: POST api/blogs. حذف مدوّنة: DELETE api/blogs/:id. ترجمة -وبتصرف- للفصل Using relational databases with Sequelize من سلسلة Deep Dive Into Modern Web Development. اقرأ أيضًا المقال السابق: أساسيات تنسيق الحاويات مقدمة عن قواعد البيانات التعامل مع قواعد البيانات مفاهيم نموذج البيانات العلائقية RDM الأساسية المهمة في تصميم قواعد البيانات مقارنة بين أنظمة إدارة قواعد البيانات العلاقية: SQLite مع MySQL مع PostgreSQL
  10. يغطِّي هذا المقال المبادئ الأكثر بساطة لتنطلق، إذ يعرِّف مفهوم العناصر elements والسمات attributes وغيرها من المفاهيم المهمة ويعرض كيفية استخدامها، ثم ينتقل إلى عرض هيكلية صفحة مكتوبة بلغة HTML وكيف تُنظَّم العناصر داخلها، بالإضافة إلى شرح ميزات أساسية مهمة أخرى، كما ستجد خلال قراءتك لهذا المقال بعض الأمثلة التطبيقية التي تعطيك فرصة لتجريب ما تتعلمه، ولا بد أن تمتلك خلفيةً بسيطةً عن الحواسب قبل أن تبدأ قراءة المقال، كما يتطلب الأمر درايةً بالبرمجيات التي ينبغي تثبيتها لبدء العمل، ودرايةً ولو بسيطة بالتعامل مع الملفات. ما هي لغة HTML؟ لا تمثِّل لغة توصيف النص التشعبي Hypertext Markup Language أو لغة HTML اختصارًا لغة برمجة، وإنما لغةً وصفيةً تساعد المتصفحات على هيكلة صفحات الويب التي تزورها، وقد تكون الصفحات بسيطةً جدًا أو معقدةً جدًا وفقًا لرؤية مطور ويب. تتألف لغة HTML من سلسلة من العناصر التي تستخدِمها في إحاطة أو تغليف أو توصيف الأجزاء المختلفة للمحتوى الذي تريد عرضه ليظهر أو يسلك سلوكًا محددًا، فقد تُستخدَم الوسوم tags المحيطة بالمحتوى لتوصيف رابط تشعبي إلى صفحة أخرى أو لإظهار نص بحروف مائلة أو غير ذلك، ولنتأمل على سبيل المثال المحتوى النصي التالي: My cat is very grumpy إذا أردت أن يكون المحتوى فقرةً نصيةً مستقلةً بذاتها، فيمكن إحاطتها بوسمَي بداية ونهاية عنصر الفقرة النصية <p>: <p>My cat is very grumpy</p> ملاحظة: لا تُعَدّ وسوم لغة HTML حساسةً لحالة الأحرف، أي من الممكن كتابتها بحروف كبيرة أو صغيرة، إذ يمكن كتابة العنصر <title> مثلًا بالصورة <TITLE> أو <Title> أو <TiTlE> وسيعمل، لكن كتابة الوسوم بأحرف صغيرة هي ممارسة تطبيقية جيدة تسهِّل قراءة الشيفرة وتُظهر استمرارية في أسلوب الكتابة. تشريح عنصر HTML لنلق نظرةً أعمق على عنصر الفقرة <P>: يتكوَّن العنصر من: وسم البداية Opening tag: يتكون من اسم العنصر -أي p في حالتنا- محاطًا بقوسَي زاوية، كما يشير هذا الوسم إلى النقطة التي يبدأ عندها العنصر أو التي يبدأ تأثيره عندها (بداية الفقرة النصية في حالتنا). وسم النهاية Closing tag: يشابه وسم البداية لكنه يبدأ بشرطة أمامية / قبل اسم العنصر، ويشير هذا الوسم إلى نهاية العنصر -أي نهاية الفقرة في حالتنا-، ويُعَدّ إغفال وسم النهاية من أكثر الأخطاء التي يرتكبها المبتدئون وقد تفضي إلى نتائج غريبة بالفعل. المحتوى Content: يشير إلى المحتوى الفعلي للعنصر وهو في حالتنا نص فقط. العنصر Element: يتكون من وسمَي البداية والنهاية وبينهما المحتوى. تطبيق: إنشاء أول عنصر في لغة HTML استخدم منطقة تحرير الشيفرة Editable code في المحرِّر المضمّن التالي لتغليف السطر المكتوب في تلك المنطقة وذلك بإضافة الوسم <em> قبل السطر والوسم <em/> بعده، إذ سيؤدي ذلك إلى ظهور السطر مكتوبًا بأحرف مائلة، وسترى نتيجة عملك في منطقة الخرج المباشر Live Output"، فإذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة ضبط Reset، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على زر "أظهر الحل Show solution" لترى الحل الصحيح. عناصر متداخلة يمكن وضع عناصر داخل عناصر أخرى أيضًا وهذا ما يُعرَف بالتداخل nesting، فإذا أردنا إظهار الكلمة "very" في الفقرة "My cat is very grumpy" بخط سميك، فيمكن تغليف هذه الكلمة داخل العنصر <strong> كما يلي: <p>My cat is <strong>very</strong> grumpy.</p> لكن عليك أن تتأكد دومًا من تداخل العناصر بصورة صحيحة، فقد فتحنا في المثال السابق العنصر <p> أولًا ثم <strong>، وبالتالي توجَّب علينا إغلاق العنصر الثاني على الصورة <strong/> ثم إغلاق الأول <p/>، وبالتالي يكون التداخل التالي غير صحيح: <p>My cat is <strong>very grumpy.</p></strong> لا بدّ من فتح وإغلاق العناصر بالصورة الصحيحة لكي تظهر بوضوح داخل أو خارج عنصر آخر، فإذا تداخلت بالصورة التي عرضناها في الشيفرة السابقة، فسيحاول المتصفح أن يخمّن بأفضل صورة ما تحاول قوله، مما قد يسبب بظهور نتائج غير متوقعة، فلا تفعل ذلك. دورة تطوير واجهات المستخدم ابدأ عملك الحر بتطوير واجهات المواقع والمتاجر الإلكترونية فور انتهائك من الدورة اشترك الآن العناصر الكتلية والعناصر السطرية تصنَّف عناصر لغة HTML ضمن مجموعتين مهمتين هما مجموعة العناصر الكتلية Block elements والسطرية inline elements: العناصر الكتلية: تشكل كتلةً مرئيةً ضمن الصفحة، كما تظهر في سطر جديد بعد العنصر الذي يسبقها، أي لا يمكن أن يقع على سطر آخر مع عنصر آخر، وسيظهر العنصر الذي يليه في سطر جديد أيضًا، كما تُعَدّ العناصر الكتلية في الصفحة عناصر هيكلة وتنظيم، فقد تمثِّل فقرةً نصيةً أو عناوين أو قوائم تعداد أو قوائم تنقل أو حواشي سفلية، وتجدر الإشارة إلى أنّ العناصر الكتلية لا توضَع ضمن عناصر سطرية لكنها قد تتداخل مع بعضها. العناصر السطرية: تقع داخل العناصر الكتلية وتحيط بأجزاء صغيرة من المحتوى، أي جزء من فقرة نصية أو مجموعة من بنود قائمة، ولا تسبب هذه العناصر ظهور سطر جديد في المستند، أي لا تشغل بنفسها السطر بالكامل، كما تُستخدَم هذه العناصر مع النصوص غالبًا، إذ ينشئ العنصر <a> مثلًا رابطًا تشعبيًا، في حين يميِّز العنصران <em> و <strong> أجزاءً من النص. انظر إلى المثال التالي: <em>first</em><em>second</em><em>third</em> <p>fourth</p><p>fifth</p><p>sixth</p> لاحظ أنّ <em> هو عنصر سطري، وكما ترى في محرر الشيفرة، ستقع العناصر الثلاث الأولى على السطر ذاته دون أية فراغات بينها، لكن من ناحية أخرى سيظهر العنصر الكتلي <p> في سطر جديد، بحيث يسبقه فراغ ويليه فراغ، وهذه الفراغات هي نتيجة لتطبيق تنسيق CSS الافتراضي لعنصر الفقرة النصية <p>. ملاحظات: أعادت لغة HTML بنسختها الخامسة -أي HTML5- تعريف فئات العناصر، وعلى الرغم من دقة التعاريف الجديدة وتفاديها للكثير من الغموض الذي يلف بعض العناصر، إلا أنها أكثر تعقيدًا موازنةً بمفهومَي العنصر الكتلي والسطري، لذلك سنبقى في مقالنا مع هذين المفهومين. لا يجب الخلط بين مفهومَي العنصر الكتلي والسطري مع أنواع صناديق CSS التي تحمل الأسماء نفسها، وعلى الرغم من الترابط الافتراضي للأسماء في الحالتين، فإنّ تغيير قيمة الخاصية display عند تنسيق العنصر لن يغير من الفئة التي ينتمي إليها العنصر أصلًا، أي لن يؤثّر على محتواه من العناصر الأخرى، لهذا تخلّت HTML5 عن مفهومَي العنصر الكتلي والسطري منعًا لهذا الالتباس الشائع. العناصر الفارغة لا تتقيد جميع العناصر بالنمط المؤلف من وسم البداية والمحتوى ثم وسم النهاية، إذ لا تحتوي بعض العناصر سوى على وسم بداية وتستخدَم عادةً في إدراج شيء ما في الصفحة، كما يُدرَج العنصر <img> على سبيل المثال على أساس صورة في الصفحة: <img src="https://raw.githubusercontent.com/mdn/beginner-html-site/gh-pages/images/firefox-icon.png"> ستكون نتيجة تنفيذ الشيفرة كما يلي: ملاحظة: تُدعى العناصر الفارغة أحيانًا بالعناصر الخالية void elements ولاحاجةً إلى وضع المحرف / قبل إغلاق الوسم، أي بالشكل <‎img ="cat.jpg" alt="cat" /‎> إلا في حالات خاصة مثل التوافق مع صيغة XML. السمات في لغة HTML يمكن أن تحمل عناصر لغة HTML سمات، وتبدو السمات كما يلي: تحتوي السمات على معلومات إضافية عن العنصر، ولا تظهر مع المحتوى الذي يعرضه، إذ تُشير السمة class في الصورة السابقة مثلًا إلى اسم يُستخدَم لتطبيق تنسيقات محددة على العنصر. ينبغي أن تُكتب السمة بحيث تراعي ما يلي: توجد مسافة فارغة بينها وبين اسم العنصر، كما ينبغي أن تفصل مسافة فارغة بينها وبين السمة التي تليها إذا وجدت. تلي اسم السمة إشارة مساواة =. توضع قيمة السمة بين إشارتي تنصيص مزدوجتين " ". تطبيق عملي: إضافة سمات إلى عنصر HTML يُدعى العنصر <a> بعنصر المربط anchor الذي يحوّل النص الموجود ضمنه إلى رابط تشعبي، كما يمكن أن تحمل المرابط سمات عدة منها: href: إن قيمة هذه السمة هي عنوان ويب للرابط التشعبي مثل "href="https://www.hsoub.com. title: تستخدَم لإدراج معلومات إضافية عن الرابط مثل وصف الصفحة التي سينقلنا إليها، أي مثل title="The Mozilla homepage"، كما تظهر هذه المعلومات على أساس تلميح فوق الرابط عندما تمرِّر مؤشر الفارة فوقه. target: تستخدَم لتحديد طريقة عرض الصفحة التي ينقلنا إليها الرابط مثل "target="_blank، إذ سيعرض لك المتصفح الآن الصفحة في نافذة جديدة، في حين سيعرض المتصفح هذه الصفحة في النافذة نفسها ما لم تستخدم هذه السمة. سنحاول تحويل السطر الموجود في منطقة تحرير الشيفرة إلى مربط كما يلي: أضف الوسم <a>. أضف الصفتين href و title. حدد طريقة عرض صفحة الرابط من خلال السمة target. سترى نتيجة عملك في منطقة عرض النتيجة، ومن المفترض أن ترى رابطًا يعرض قيمة السمة titleعندما تمرِّر المؤشر فوقه، وينقلك عند النقر عليه إلى الصفحة التي حددتها في السمة href، وتذكَّر أن تضع مسافة فارغة بين اسم العنصر والسمات، فإذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر "إعادة الضبط Reset"، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على الزر "أظهر الحل Show solution" لترى الحل الصحيح. السمات المنطقية قد تلاحظ أحيانًا سمات دون قيم، وتُدعى هذه السمات بالسمات المنطقية Boolean، إذ تمتلك هذه السمات قيمةً واحدةً وعادةً ما تكون هذه القيمة مطابقةً لاسم السمة، ونضرب مثلًا على ذلك السمة disabled التي تضاف إلى عنصر الإدخال النصي، بحيث تمنع المستخدِم من الكتابة ضمن مربع النص الذي يبدو رماديًا: <input type="text" disabled="disabled"> يمكن أن نكتب السمة المنطقية بصيغة مختصرة كما يلي: <!-- لمنع المستخدم من الكتابة ضمن المربع النصي disabled استخدام السمة --> <input type="text" disabled> <!-- لا يضم عنصر الإدخال النصي هذه السمة ويمكن للمستخدم الكتابة ضمنه --> <input type="text"> تعطيك الشيفرة في فقرة "السمات المنطقية" النتيجة التالية عند تنفيذها: حذف إشارتي التنصيص المحيطتين بقيمة السمة ستلاحظ خلال متابعتك لشيفرات العديد من المواقع وجود بعض الأساليب الغريبة في كتابة الشيفرة، منها وجود سمات لا توُضع قيمها بين إشارتَي تنصيص، إذ يُسمح بهذا الأمر في حالات محددة وقد يُضِر شيفرتك في أخرى، كما يمكن مثلًا كتابة السمة href في مثال المربط السابق كما يلي: <a href=https://www.mozilla.org/>favorite website</a> لكن ستظهر المشاكل عندما نطبِّق الأمر نفسه على السمة title: <a href=https://www.mozilla.org/ title=The Mozilla homepage>favorite website</a> سيخطئ المتصفح في تفسير المطلوب وسيفهم السمة title على أنها ثلاث سمات الأولى هي title وقيمتها "The" وسِمتان منطقيّتان هما Mozilla و homepage، إذ يسبب هذا الأمر أخطاءً غير متوقعة أو سلوكًا غير متوقع كما سترى في نتيجة التنفيذ التالية: لهذا السبب، يجب أن تضع قيمة السمات ضمن إشارتَي تنصيص دومًا لتجنب مشاكل مثل هذه ولتسهِّل قراءة الشيفرة. استخدام إشارتي تنصيص مزدوجتين أو مفردتين ستلاحظ في هذه المقال أننا وضعنا قيم السمات ضمن إشارتَي تنصيص مزدوجتين " "، لكنك قد ترى في مواقع أخرى قيمًا محاطةً بإشارتَي تنصيص مفردتين ' '، والأمر برمته مسألة ذوق، إذ يمكنك اختيار الأسلوب الذي تريد، أي تعطي كلا الكتابتين في المثال التالي النتيجة نفسها: <a href="https://www.example.com">A link to my example.</a> <a href='https://www.example.com'>A link to my example.</a> انتبه إلى عدم المزج بين إشارتَي التنصيص، إذ سيقود المزج الذي سنعرضه في مثالنا التالي إلى أخطاء: <a href="https://www.example.com'>A link to my example.</a> إذا أردت وضع إشارة تنصيص محددة داخل قيمة السمة، فعليك إحاطة القيمة كلها بإشارتَي التنصيص الأخرى كما يلي: <--! "" ضع قيمة السمة بين مزدوجتين title لتضع الإشارة ' ضمن قيمة السمة --> <a href="https://www.example.com" title="Isn't this fun?">A link to my example.</a> استخدم كيانات HTML لتضع إشارة تنصيص ضمن إشارتَي تنصيص من النوع نفسه -أي مفردة ضمن مفردتين مثلًا-، إذ تُعَدّ الكتابة التالية خاطئةً: <a href='https://www.example.com' title='Isn't this fun?'>A link to my example.</a> عليك استخدام كيان HTML الذي يعطي المحرف ' وهو مجموعة المحارف ;apos&: <a href='https://www.example.com' title='Isn&apos;t this fun?'>A link to my example.</a> تشريح مستند HTML لا فائدة كبيرة من عناصر لغة HTML بمفردها، وإنما عليك تعلّم كيفية تنظيمها لتبني صفحة ويب بأكملها: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>My test page</title> </head> <body> <p>This is my page</p> </body> </html> إليك الطريقة: أولًا: استخدم العنصر: <!DOCTYPE html> لقد كانت الغاية من هذا العنصر في الأيام الأولى (1991/1992) أن يعمل على أساس رابط إلى مجموعة من القواعد التي ينبغي أن تحققها صفحة مكتوبة بلغة HTML لكي تُعَدّ صفحةً جيدةً، إذ يبدو هذا العنصر كما يلي: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> يُستخدَم حاليًا هذا العنصر في بداية المستند حتى يعمل كل شيء بالطريقة الصحيحة، إذ تمثل الشيفرة <!DOCTYPE html> أقل مجموعةً من المحارف كي يكون هذا العنصر صالحًا. ثانيًا: استخدم العنصر <html></html> الذي يضم محتوى الصفحة بأكمله، ويعرف أحيانًا بالعنصر الجذري. ثالثًا: استخدم العنصر <head></head> الذي يعمل على أساس حاوية لكل ما تريد وضعه في صفحة HTML دون أن يكون جزءًا من المحتوى المعروض للزائر، ويتضمن ذلك الكلمات المفتاحية ووصف الصفحة الذي يظهر في نتائج محركات البحث وملفات تنسيق CSS ومجموعة المحارف الكتابية المستخدَمة وغيرها. رابعًا: استخدم العنصر <"meta charset="utf-8> الذي يضبط مجموعة المحارف التي تستخدِمها في الصفحة، وهنا اخترنا المجموعة UTF-8 التي تضم محارف الأغلبية الساحقة من اللغات المكتوبة، إذ تستطيع هذا المحارف أن تعرض الآن أيّ محتوى نصي بأي لغة قد تضعه في صفحتك، ولا مبرر لعدم ضبط مجموعة المحارف المستخدَمة، كما ستساعدك على تحاشي الكثير من الأخطاء لاحقًا. خامسًا: استخدم العنصر <title></title> الذي يضبط عنوان صفحتك أعلى المتصفح عند تحميل الصفحة، كما يُستخدَم لوصف الصفحة عندما تضيفها إلى قائمة الصفحات المفضلة. سادسًا: استخدم العنصر <body></body> الذي يضم المحتوى الذي تريد عرضه على زائرِي صفحتك بأكمله، سواءً كان نصًا أو صورًا أو فيديو أو ألعاب أو أيّ شيء آخر. تطبيق عملي: إضافة بعض الميزات إلى مستند HTML إذا أردت تجريب كتابة شيفرة HTML على جهازك، فيمكنك تنفيذ النقاط التالية: نسخ المثال السابق عن هيكلية صفحة HTML. إنشاء ملف نصي جديد باستخدام محرر النصوص لديك. لصق الشيفرة في الملف. حفظ الملف باسم index.html. ملاحظة: ستجد ايضًا قالب HTML الأساسي ضمن المستودع المخصص فريق مطوري موزيللا على جيت-هاب. افتح الملف باستخدام متصفحك لترى نتيجة تصيير الشيفرة، وعدّل الشيفرة كما تشاء وحدِّث ما يعرضه المتصفح لإظهار أيّة تغييرات، إذ ستبدو الصفحة بشكلها الأصلي كما يلي: يمكنك التعديل على الشيفرة في هذا التمرين على حاسوبك كما شرحنا سابقًا أو من خلال المحرر المدمج مع المقال والذي نعرضه بين الفينة والأخرى، كما يمكنك تعزيز مهاراتك بإنجاز المهام التالية: أضف عنوانًا رئيسيًا للصفحة تحت العنصر <body> مباشرةً، إذ يجب أن تضع العنوان بين وسم البداية <h1> ووسم النهاية <h1/>. عدِّل محتوى الفقرة النصية لتتضمن نصًا من اختيارك. أبرز الكلمات الهامة في نصك بتغليفها ضمن العنصر <strong>. أضف رابطًا ضمن الفقرة النصية بالطريقة التي شرحناها سابقًا. أضف صورةً إلى صفحتك تحت الفقرة النصية، إذ ستحصل على نقاط إضافية إذا تمكنت من إنشاء رابط إلى صورة أخرى على حاسوبك الشخصي أو على ويب. إذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر "إعادة الضبط Reset"، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على الزر "أظهر الحل Show solution" لترى الحل الصحيح. المسافات الفارغة في HTML لاحظ استخدامنا للكثير من المسافات الفارغة في شيفرة الصفحة، وهذا أسلوب كتابة قد تعتمده، إذ سيعطي أسلوبَي كتابة الشيفرة التاليين النتيجة ذاتها: <p>Dogs are silly.</p> <p>Dogs are silly.</p> مهما أضفت من مسافات فارغة ضمن محتوى العنصر الذي قد يتضمن مسافات فارغة عدة وربما محرف الانتقال إلى سطر جديد، فسيختزِل محلل HTML كل سلسلة متلاحقة من المسافات الفارغة إلى مسافة فارغة واحدة، فلماذا إذًا نضيف فراغات أكثر؟ القضية تتعلق بسهولة القراءة. من السهل فهم ما فعلته أثناء كتابتك لشيفرة الصفحة إذا رتبتها جيدًا، فما نفعله عادةً أثناء كتابة الشيفرة هو إزاحة العناصر بمقدار مسافتين فارغتين عن العنصر الذي تقع داخله، ويعود الأمر إليك دائمًا في اختيار التنسيق الذي تراه مناسبًا لتنظيم الشيفرة، ومن الأفضل دائمًا تنسيقها. كيانات لغة HTML: إضافة محارف خاصة تُعَدّ المحارف < و > و " و ' و & في HTML محارف خاصة لكونها جزءًا من الصياغة القواعدية للغة، فكيف سنتمكن إذًا من إضافة هذه المحارف إلى محتوى العناصر؟ وكيف سنضع مثلًا إشارة "أكبر من" دون أن تؤثر على تفسير الشيفرة؟ يمكن ذلك من خلال مراجع إلى تلك المحارف، والمراجع هي رموز خاصة تمثِّل محرفًا محددًا يمكن استخدامه في هذه الحالة، كما يبدأ كل مرجع بالمحرف & وينتهي بالمحرف ;. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المحرف الخاص سلسلة الحروف المرجعية < ;lt& > ;gt& " ;quot& ' ;apos& & ;amp& يمكن تذكر المرجع إلى المحرف الخاص بسهولة لأن النص الذي يشير إلى إشارة أصغر مثلًا ;lt& يمكن أن يُفهم من المصطلح الذي أخذ منه أي Less Than، والأمر مشابه لبقية المراجع. ستجد في المثال التالي فقرتين نصيتين: <p>In HTML, you define a paragraph using the <p> element.</p> <p>In HTML, you define a paragraph using the <p> element.</p> ستُعرض الفقرة الأولى بطريقة غير صحيحة عند التنفيذ، إذ يفسر المتصفح الوسم <p> المكتوب ضمن محتوى الفقرة على أنه بداية فقرة نصية جديدة، في حين ستبدو الفقرة النصية الثانية صحيحةً عند استخدام مراجع إلى المحارف. ملاحظة: لا حاجة إلى استخدام مراجع إلى كيانات الرموز الأخرى، لأن المتصفحات الحديثة ستتعامل مع جميع الرموز جيدًا إذا كانت UTF-8 هي مجموعة محارف المستخدَمة في ترميز HTML. التعليقات في لغة HTML زوِّدت لغة HTML بآلية لإدراج تعليقات ضمن الشيفرة يتجاهلها المتصفح عند تصيير الصفحة ولا تُعرض للزائر، والغاية من التعليقات هي إضافة ملاحظات إلى الشيفرة بغية شرحها أو توضيح منطق العمل، كما تظهر فائدة التعليقات عندما تضطر إلى العمل على شيفرة أنجزتها قبل فترة من الزمن ولم تَعُد تتذكر تفاصيل العمل بأكمله، كما تظهر أهميتها عندما يعمل على تغيير الشيفرة وتحديثها مجموعة من الأشخاص، ولإدراج تعليق، ضعه ضمن الوسمين <-- و --!> كما في المثال التالي: <p>I'm not inside a comment</p> <!-- <p>I am!</p> --> لاحظ أنّ ما سيعرضه المتصفح هو الفقرة الأولى فقط. خلاصة لقد وصلت إلى نهاية المقال، ونتمنى أن تكون قد استمتعت بما جاء فيه، إذ يفترض الآن أنك فهمت ماهية لغة HTML وكيف تعمل بأبسط صورها، كما من المفترض أيضًا أنك أصبحت قادرًا على كتابة بعض العناصر والسمات، كما سنتعمق في المقالات اللاحقة في بعض المفاهيم التي طرحناها وسنضيف مفاهيم جديدة. وطالما أنك شرعت في تعلم لغة HTML، فننصحك بتعلم مبادئ تنسيق الصفحات باستخدام CSS، فهي تعمل جيدًا مع لغة HTML كما سترى لاحقًا. ترجمة -وبتصرف- للمقال Getting started with HTML. اقرأ أيضًا نحو فهم أعمق لتقنيات HTML5 مكونات الويب: عناصر HTML المخصصة وقوالبها HTML و CSS للمبتدئين: كيف تصمم أول صفحة ويب لك
  11. نتعرف في هذا المقال على وضع تطبيق ويب بأكمله مع جميع خدماته ضمن حاويات دوكر Docker وتنسيقها وضبط إعداداتها. استخدام الحاويات مع React سنحاول أن ننشئ تطبيق React ونضعه ضمن حاوية تاليًا، لهذا سنختار npm مديرَا للحزم علمًا أن yarn هو المدير الافتراضي لبرنامج create-react-app: $ npx create-react-app hello-front --use-npm ... Happy hacking! يُثبت create-react-app كل الاعتماديات اللازمة، فلا حاجة لتنفيذ الأمر npm install. ستكون الخطوة الثانية تحويل شيفرة JavaScript و CSS إلى ملفات ساكنة جاهزة لمرحلة الإنتاج. أمّا create-react-app فلديه build ليكون سكربت npm، لهذا سنستفيد من هذه الناحية: $ npm run build ... Creating an optimized production build... ... The build folder is ready to be deployed. ... أما الخطوة الأخيرة هي التفكير بطريقة للعمل مع خادم لتقديم الملفات الساكنة. يمكننا الاستفادة من express.static مع خادم Express لهذا الغرض، لهذا سأترك الموضوع تمرينًا لك، وسننتقل بدلًا من ذلك إلى كتابة ملف Dockerfile: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci RUN npm run build يبدو ما كتبناه صحيحًا تقريبًا، لهذا سنبنيه ونقيّم مسارنا، والهدف أن نبني التمرين دون أخطاء. سنحاول بعد ذلك أن نتحقق من وجود الملفات داخل الحاوية من خلال أوامر "bash". $ docker build . -t hello-front [+] Building 172.4s (10/10) FINISHED $ docker run -it hello-front bash root@98fa9483ee85:/usr/src/app# ls Dockerfile README.md build node_modules package-lock.json package.json public src root@98fa9483ee85:/usr/src/app# ls build/ asset-manifest.json favicon.ico index.html logo192.png logo512.png manifest.json robots.txt static الخيار الصحيح لتخديم الملفات الساكنة ضمن الحاوية هو serve نظرًا لوجود Node ضمن الحاوية. لنحاول تثبيت serve لتخديم الملفات الساكنة ونحن داخل الحاوية: root@98fa9483ee85:/usr/src/app# npm install -g serve added 88 packages, and audited 89 packages in 6s root@98fa9483ee85:/usr/src/app# serve build ┌───────────────────────────────────┐ │ │ │ Serving! │ │ │ │ Local: http://localhost:5000 │ │ │ └───────────────────────────────────┘ أغلق الحاوية باستخدام الاختصار "Ctrl+C" لإضافة بعض التوجيهات إلى ملف Dockerfile. يتحوّل تثبيت serve إلى عملية تشغيل RUN في ملف Dockerfile، وهكذا ستُثبّت الاعتمادية أثناء عملية البناء، وسيكون أمر تخديم مجلد البناء هو نفسه أمر تشغيل الحاوية: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci RUN npm run build RUN npm install -g serve CMD ["serve", "build"] يتضمن الأمر ‍‍CMD الآن قوسين مربعين ونكون بذلك قد استخدمنا ما يُعرف بالشكل التنفيذي exec form من الأمر ‍‍CMD، علمًا أنه يُكتب بثلاثة أشكال لكن الشكل السابق هو المُفضّل. عندما نبني الصورة الآن باستخدام الأمر: docker build . -t hello-front ثم تشغيلها باستخدام الأمر: docker run -p 5000:3000 hello-front سيكون التطبيق متاحًا على العنوان "http://localhost:5000". استخدام المراحل المتعددة على الرغم من أن serve خيار صحيح لكن بالإمكان إيجاد بديل أفضل، إذ أن الغاية هنا هي إنشاء صورة لا تحتوي أي شيء غير مطلوب وبأقل عدد من الاعتماديات، وبالتالي ينخفض احتمال توقف الصورة عن العمل أو أن تصبح عرضةً للهجمات مع الوقت. صُمّمت عملية البناء متعددة المراحل Multi-stage builds لتفصل عملية البناء إلى مراحل مختلفة، ومن الممكن حينها تحديد ملفات الصورة التي يُسمح لها بالانتقال من مرحلة إلى أخرى، كما يتيح ذلك أيضًا إمكانية التحكم بحجم الصورة، فلن نحتاج إلى جميع الملفات الجانبية التي تنتج عن عملية البناء ضمن الصورة الناتجة. إنّ الصورة الأصغر أسرع في التحميل والتنزيل وتساعد في تخفيض عدد نقاط الضعف في برنامجك. عند استخدام البناء متعدد المراحل، بالإمكان الاعتماد على حلول تتبع نهج المحاولة والخطأ مثل استخدام الخادم Nginx في إدارة الملفات الساكنة دون عناء شديد. تدلنا صفحة استخدام Nginx مع Docker Hub على المعلومات الضرورية لفتح المنافذ واستضافة محتوى ساكن. لنستخدم الآن ملف Dockerfile السابق بعد تغيير ما يلي التعليمة FROM لإضافة اسم المرحلة: # called build-stage الأولى الآن إلى مرحلة تُدعى FROM تشير تعليمة FROM node:16 AS build-stage WORKDIR /usr/src/app COPY . . RUN npm ci RUN npm run build # هذه مرحلة جديدة الآن، وسيختفي كل شيء قبلها ما عدا الملفات التي نريد نسخها FROM nginx:1.20-alpine # /usr/share/nginx/html إلى build-stage نسخ مجلد البناء من # docker hub page وجد الموقع الوجهة من خلال صفحة شروحات COPY --from=build-stage /usr/src/app/build /usr/share/nginx/html لقد صرحنا أيضًا عن مرحلة أخرى تُنقل إليها فقط الملفات الضرورية من المرحلة الأولى (المجلد "build" الذي يضم المحتوى الساكن). بعد بنائها ثانيةً، ستكون الصورة جاهزة لتخديم المحتوى الساكن. إن المنفذ الافتراضي لخادم Nginx هو 80 وبالتالي سينفع تنفيذ الأمر p 8000:80-، لهذا لا بد من تغيير معاملات أمر التشغيل قليلًا. تنطوي عملية البناء متعددة المراحل على تحسينات داخلية قد تؤثر على عملية البناء، إذ تتجاوز عملية البناء متعددة المراحل مثلًا المراحل التي لم تُستخدم، وبالتالي علينا تمرير بعض البيانات إلى المرحلة القادمة إن أردنا استخدام مرحلة ما لتبديل جزء من خط البناء مثل الاختبارات أو التنبيهات. لهذا الأمر تبريراته في بعض الحالات كأن تَنسخ الشيفرة من مرحلة الاختبار إلى مرحلة البناء كي تضمن بناء الشيفرة المُختبرة. التمرينان 12.13 و 12.14 حاول أن تحل التمرينين التاليين: التمرين 12.13: الواجهة الامامية لتطبيق المهام لقد وصلنا أخيرًا إلى الواجهة الأمامية، لهذا عليك أن تلقي نظرة على محتويات المجلد "todo-app/todo-frontend" وتقرأ ملف "اقرأني README". ابدأ بتشغيل الواجهة الأمامية خارج الحاوية، وتأكد من تناغم عملها مع الواجهة الخلفية. ضع التطبيق ضمن الحاوية بعد ذلك أنشئ الملف "todo-app/todo-frontend/Dockerfile" ثم استخدم التعليمة ENV لتمرير متغير البيئة REACT_APP_BACKEND_URL إلى التطبيق وشغّله مع الواجهة الخلفية. ينبغي أن تعمل الواجهة الخلفية حاليًا خارج الحاوية. وانتبه إلى ضرورة ضبط المتغير REACT_APP_BACKEND_URL قبل بناء الواجهة الأمامية وإلا لن يُعرَّف ضمن الشيفرة. التمرين 12.14: إجراء الاختبارات أثناء عملية البناء من الميزات الهامة للبناء متعدد المراحل، استخدام مرحلة البناء لإجراء الاختبارات على التطبيق testing. فإن أخفقت مرحلة الاختبار ستُخفق عملية البناء بأكملها. لكن تنفيذ الاختبارات جميعها خلال عملية بناء الصورة ليست فكرة جيدة؛ لهذا قد تكون الاختبارات المتعلقة بالحاوية هي الأنسب. استخلص مكوّن Todo يمثل مهمة واحدة من الشيفرة ، ثم اكتب اختبارًا للمكوّن الجديد وأضف تنفيذ الاختبارات إلى عملية البناء. نفّذ الاختبار من خلال الأمر CI=true npm test وإلا سيبدأ برنامج create-react-app بمراقبة التغييرات مسببًا توقف خط العمل pipeline. بإمكانك إضافة مرحلة بناء جديدة لإجراء الاختبار إن أردت ذلك. لكن تذكر في هذه الحالة أن تقرأ آخر فقرة قبل التمرين 12.13 مجددًا. التطوير ضمن الحاويات لننقل الآن تطبيق المهام بأكمله إلى حاوية. وإليك بعض الأسباب التي قد تدفعنا إلى ذلك: للإبقاء على تماثل بيئة العمل بين التطوير والإنتاج تفاديًا للثغرات التي تظهر فقط في بيئة التشغيل. لتفادي الاختلافات بين المطورين وبيئات عملهم الخاصة والتي تقود إلى صعوبات أثناء تطوير التطبيق. لمساعدة أعضاء الفريق الجدد على البدء بتثبيت بيئة تشغيل الحاوية دون الحاجة إلى أي شيء آخر. في المقابل، قد نواجه سلوكًا غير معهود عندما لا نشغّل التطبيق كما اعتدنا، ولهذا لا بد من فعل ما يلي على الأقل لنقل التطبيق إلى الحاوية: تشغيل التطبيق في وضع التطوير. الوصول إلى الشيفرة من خلال برنامج VSCode. لنبدأ بالواجهة الأمامية، وطالما أن ملف Dockerfile لمرحلة التطوير سيختلف تمامًا عن ملف Dockerfile لنسخة الإنتاج، سننشئ ملف جديد اسمه "dev.Dockerfile". لنشغّل create-react-app في وضع التطوير ومن المفترض أن يكون الأمر بسيطًا على النحو التالي: FROM node:16 WORKDIR /usr/src/app COPY . . # طالما سنعمل في وضع التطوير npm install إلى npm ci غيّر RUN npm install # npm start إن أمر التشغيل في وضع التطوير هو CMD ["npm", "start"] تُستخدم الراية f- أثناء البناء لتحديد الملف الذي يُستخدم، وإلا سيقع الاختيار على الملف Dockerfile، لهذا يكون أمر بناء الصورة على النحو التالي: docker build -f ./dev.Dockerfile -t hello-front-dev . سيُخدَّم create-react-app على المنفذ 3000، لهذا يمكنك اختباره بتشغيل الحاوية على هذا المنفذ. تقتضي المهمة الثانية الوصول إلى الملفات باستخدام VSCode، وهناك على الأقل طريقتان لإنجاز الأمر: باستخدام الموسِّع Visual Studio Code Remote - Containers. باستخدام الأقراص volumes، وبنفس طريقة تخزين البيانات في قاعدة البيانات. لنتجاوز المهمة الثانية كوننا سنضطر فيها إلى التعامل مع محررات أخرى، ولنجرب تشغيل الحاوية مع الراية v-، فإذا جرى كل شيء على ما يرام ننقل الإعدادات إلى ملف docker-compose. لاستخدام تلك الراية علينا تزويدها بالمجلد الحالي من خلال تنفيذ الأمر pwd الذي يعطي المسار إلى المجلد الحالي. حاول أن تنفِّذ ذلك من خلال الأمر (echo $(pwd في واجهة سطر الأوامر لديك وبالترتيب التالي: $ docker run -p 3000:3000 -v "$(pwd):/usr/src/app/" hello-front-dev Compiled successfully! You can now view hello-front in the browser. بإمكاننا الآن تعديل الملف "src/App.js" وستُعرض التغييرات مباشرةً على المتصفح. سننقل تاليًا الإعدادات إلى الملف "docker-compose.yml" الذي ينبغي أن يكون موجودًا في جذر المشروع: services: app: image: hello-front-dev build: context: . # يختار السياق هذا المجلد ليكون سياق البناء dockerfile: dev.Dockerfile # الذي سيُستخدم Dockerfile لاختيار ملف volumes: - ./:/usr/src/app # يمكن أن يكون المسار نسبي, so ./ is enough to say # ./ لهذا يكفي استخدام #docker-compose.yml للقول أنه نفس مكان وجود الملف ports: - 3000:3000 container_name: hello-front-dev # hello-front-dev لتسمية الحاوية بالاسم يمكننا بهذه الإعدادات الآن تشغيل التطبيق في وضع التطوير من خلال الأمر docker-compose up، ولن تحتاج حتى إلى تثبيت Node. يسبب تثبيت اعتماديات جديدة عدة مشاكل في إعداد بيئة تطوير كهذه، لهذا ستجد أن تثبيت الاعتمادية الجديدة ضمن الحاوية هو أحد الخيارات الجيدة. فبلدلًا من تنفيذ الأمر التالي مثلًا npm install axios ، ثبّت هذه الاعتمادية ضمن الحاوية التي تعمل من خلال الأمر docker exec hello-front-dev npm install axios أو أضفها إلى الملف ثم نفِّذ الأمر docker build من جديد. التمرين 12.15: إعداد بيئة تطوير الواجهة الأمامية أنشئ الملف واستخدم الأقراص لتمكين تطوير الواجهة الأمامية لتطبيق المهام عندما يعمل ضمن الحاوية. التواصل بين الحاويات في شبكة Docker تهيئ الأداة Docker شبكةً بين الحاويات وتضيف خادمًا لأسماء النطاقات DNS لربط أي حاويتين بسهولة. دعونا إذًا نضيف خدمة جديدة إلى وسنرى كيف تعمل الشبكة وخادم DNS. سنستخدم الحزمة التنفيذية Busybox التي تضم مجموعةً من الأدوات التي قد تحتاجها وتُعرف هذه الحزمة باسم "سكين الجيش السويسري الخاصة بنظام Linux المدمج". لهذا يمكننا بالتأكيد الاستفادة منها. تساعدنا Busybox في تنقيح إعداداتنا، لهذا إن لم تتمكن من حل التمرين السابق، عليك استخدام Busybox لمعرفة ما يعمل من إعداداتك وما لا يعمل. لنختبر ما قلناه الآن. تتواجد تلك الحاويات ضمن شبكة ويمكنك الربط بينها بسهولة، ويمكن إضافة Busybox إلى الخلطة بتغيير الملف "docker-compose.yml" إلى: services: app: image: hello-front-dev build: context: . dockerfile: dev.Dockerfile volumes: - ./:/usr/src/app ports: - 3000:3000 container_name: hello-front-dev Debug-helper: image: busybox لن تتضمن حاوية Busybox على أية عمليات تجري ضمنها لذلك يمكننا تنفيذ الأمر exec. عندها سيبدو الخرج الناتج عن تنفيذ التعليمة docker-compose up على النحو التالي: $ docker-compose up Pulling debug-helper (busybox:)... latest: Pulling from library/busybox 8ec32b265e94: Pull complete Digest: sha256:b37dd066f59a4961024cf4bed74cae5e68ac26b48807292bd12198afa3ecb778 Status: Downloaded newer image for busybox:latest Starting hello-front-dev ... done Creating react-app_debug-helper_1 ... done Attaching to react-app_debug-helper_1, hello-front-dev react-app_debug-helper_1 exited with code 0 hello-front-dev | hello-front-dev | > react-app@0.1.0 start hello-front-dev | > react-scripts start هذا الخرج متوقع كون الحزمة هي مجموعة أدوات مثل غيرها. لنستخدم الحزمة في إرسال طلب إلى الحاوية "hello-front-dev" لنرى كيف يعمل خادم DNS. بإمكاننا تنفيذ الطلب wget أثناء عمل الحاوية فهو أداةٌ موجودةٌ ضمن Busybox مهمتها إرسال طلب إلى الحاوية hello-front-dev من مساعد التنقيح debug-helper. يمكننا استخدام الأمر docker-compose run SERVICE COMMAND لتنفيذ خدمة مع أمر محدد، ويتطلب استخدام الأمر wget السابق الراية O- تليها - لنقل الاستجابة إلى مجرى الخرج: $ docker-compose run debug-helper wget -O - http://app:3000 Creating react-app_debug-helper_run ... done Connecting to hello-front-dev:3000 (172.26.0.2:3000) writing to stdout <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> ... يُعد عنوان URL الجزء المهم هنا، فهو يشير إلى أننا اتصلنا بالخدمة "hello-front-dev" والمنفذ 3000. لقد منحنا الحاوية اسمها "hello-front-dev" باستخدام التعليمة container_name في ملف "docker-compose"، أما المنفذ فهو المنفذ المتاح للوصول إلى التطبيق ضمن الحاوية. ولا حاجة لنشر المنفذ كي تتصل به بقية الخدمات الموجودة على نفس الشبكة، فالمنافذ المحددة في الملف "docker-compose" هي للوصول الداخلي وحسب. دعونا نغيّر رقم المنفذ في الملف "docker-compose.yml" لتوضيح الأمر: services: app: image: hello-front-dev build: context: . dockerfile: dev.Dockerfile volumes: - ./:/usr/src/app ports: - 3210:3000 container_name: hello-front-dev debug-helper: image: busybox سيُتاح التطبيق على الحاسوب المضيف وعلى العنوان http://localhost:3210 عند تنفيذ الأمر docker-compose up، لكن التطبيق لا يزال يعمل وفقًا للأمر السابق docker-compose run debug-helper wget -O - http://app:3000 طالما أن المنفذ هو 3000 أيضًا ضمن شبكة دوكر. يطلب الأمر docker-compose run -كما تشرح الصورة السابقة-من مساعد التنقيح أن يرسل طلبًا ضمن شبكة دوكر docker بينما يرسل المتصفح في الجهاز المضيف الطلب من خارج الشبكة. أما الآن وقد علمت سهولة إيجاد الخدمات في الملف "docker-compose.yml" وليس لدينا أي شيء للتنقيح، سنزيل مساعد التنقيح و نُعيد المنافذ إلى 3000:3000 في الملف "docker-compose.yml". التمرين 12.16: تشغيل الواجهة الخلفية لتطبيق المهام ضمن حاوية التطوير استخدم الأقراص والمكتبة Nodemon لتمكين عملية تطوير الواجهة الخلفية لتطبيق المهام وهي تعمل ضمن الحاوية. أنشئ الملف "todo-backend/dev.Dockerfile" وعدّل الملف "todo-backend/docker-compose.dev.yml". عليك إعادة التفكير أيضًا في الاتصالات بين الواجهة الخلفية و قاعدة البيانات MongoDB، أو Redis. ولحسن الحظ يدعم docker-compose استخدام متحولات بيئة يمكن تمريرها إلى التطبيق: services: server: image: ... volumes: - ... ports: - ... environment: - REDIS_URL=... - MONGO_URL=... وُضعت عناوين URL للخادم المحلي بطريقة خاطئة عمدًا وعليك وضع القيم الصحيحة. وتذكر أن تراقب دائمًا ما يحدث على شاشة الطرفية، فقد ستلمّح رسائل الخطأ إلى مكان المشكلة إن حدث خلل ما. إليك هذه الصورة التوضيحة التي قد تنفع في توضيح الاتصالات ضمن شبكة docker: التواصل بين الحاويات في بيئة أكثر حيوية سنضيف تاليًا خادم وكيل معكوس reverse proxy إلى الملف " docker-compose.yml". واستنادًا إلى ويكيبيديا: سيكون الخادم الوكيل المعكوس في حالتنا نقطة دخول مفردة إلى تطبيقنا، أم الهدف النهائي فهو إعداد واجهة React الأمامية و واجهة Express الخلفية معًا خلف الخادم والوكيل المعكوس. لديك عدة خيارات تساعدك في إنجاز الخادم الوكيل، مثل Traefik و Caddy و Nginx و Apache وقد رُتبت من الأحدث إلى الأقدم ظهورًا، لكن خيارنا سيكون Nginx. لنضع الآن الواجهة الأمامية المتمثلة بالحاوية "hello-frontend" خلف الخادم الوكيل المعكوس. لهذا عليك إنشاء الملف "nginx.conf" في جذر المشروع واتّبع القالب التالي بمثابة نقطة انطلاق. لا بُد من إنجاز بعض التعديلات الثانوية لتشغيل التطبيق: # الأحداث مطلوبة لكن لا بأس بالاعتماد على الأحداث الافتراضية events { } # 80 يستمع إلى المنفذ http خادم http { server { listen 80; # (/) تُعالج الطلبات التي تبدأ بالمحرف location / { # نحتاج الأسطر الثلاثة التالية للتحميل المباشر للتغييرات proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; # http://localhost:3000 تُوجَّه الطلبات مباشرةً إلى العنوان proxy_pass http://localhost:3000; } } } أنشئ تاليًا خدمة Nginx ضمن الملف "docker-compose.yml"، ثم أضف قرصًا كما ورد في إرشادات الصفحة الرسمية للأداة Docker Hub حيث يكون الجانب الأيمن على الشكل هو: etc/nginx/nginx.conf:ro/:،إذ يدل التصريح ro على أن القرص للقراءة فقط: services: app: # ... nginx: image: nginx:1.20.1 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro ports: - 8080:80 container_name: reverse-proxy depends_on: - app # انتظر حاوية الواجهة الخلفية حتى تُقلع يمكنك الآن تنفيذ الأمر ومراقبة نتيجة العمل: $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a02ae58f3e8d nginx:1.20.1 "/docker-entrypoint.…" 4 minutes ago Up 4 minutes 0.0.0.0:8080->80/tcp, :::8080->80/tcp reverse-proxy 5ee0284566b4 hello-front-dev "docker-entrypoint.s…" 4 minutes ago Up 4 minutes 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp hello-front-dev عند الانتقال إلى العنوان http://localhost:8080 ستظهر صفحة الحالة 502 المألوفة، وهذا لأن توجيه الطلبات إلى العنوان http://localhost:3000 لن يقود إلى أي مكان، لأن حاوية الخادم Nginx لا تحتوي أي تطبيق يعمل على المنفذ 3000. إن مصطلح "خادم محلي" يُستخدم عادة للدلالة على الحاسوب الحالي الذي نلج إليه، بينما يكون الخادم المحلي فريدًا في عالم الحاويات لكل حاوية ويقود إلى الحاوية نفسها. لنختبر ذلك بالدخول إلى الحاوية Nginx واستخدام الأداة curl لإرسال طلب إلى التطبيق نفسه، وتشابه هذه الأداة من حيث الطريقة التي نستخدمها الأداة wget لكنها لا تحتاج أية رايات: $ docker exec -it reverse-proxy bash root@374f9e62bfa8:/# curl http://localhost:80 <html> <head><title>502 Bad Gateway</title></head> ... يمكننا الاستفادة من الشبكة التي يهيئها docker-compose عند تنفيذ الأمر docker-compose up، فهي تضيف كل الحاويات الموجودة في الملف "docker-compose.yml" إلى الشبكة. يتأكد خادم DNS من إمكانية إيجاد بقية الحاويات، وتُمنح كل منها اسمان الأول اسم الخدمة والآخر اسم الحاوية. طالما أننا ضمن الحاوية، سنختبر خادم DNS، لنستخدم إذًا curl لطلب الخدمة التي تُدعى (app) على المنفذ 3000 root@374f9e62bfa8:/# curl http://app:3000 <!DOCTYPE html> <html lang="en"> <head> ... <meta name="description" content="Web site created using create-react-app" /> ... هذا كل ما في الأمر. لنبدل الآن عنوان proxy_pass في الملف "nginx.conf" بهذا العنوان (http://app:3000). إن ظهرت الصفحة 502 مجددًا ، تأكد من بناء create-react-app أولًا، واقرأ الخرج الناتج عن تنفيذ الأمر docker-compose up. أمر آخر: لقد أضفنا الخيار depends_on إلى الإعدادات لنتأكد أن حاوية "nginx" لن تعمل قبل حاوية الواجهة الأمامية "app": services: app: # ... nginx: image: nginx:1.20.1 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro ports: - 8080:80 container_name: reverse-proxy Depends_on: - app إن لم نفرض تسلسل الإقلاع هذا باستخدام الخيار depends_on فقد نقع في خطر فشل إقلاع لأنه يحاول تحليل أسماء نطاقات DNS التي يُشار إليها في ملف الإعدادات: http { server { listen 80; location / { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_pass http://app:3000; } } } تجدر الملاحظة أن الخيار depends_on لا يضمن أن تكون الخدمة في الحاوية جاهزةً للعمل، بل يضمن أن الحاوية قد بدأت العمل وأضيف المُدخل الخاص بها إلى خادم DNS، لكن إذا أردت من خدمة أن تنتظر أخرى حتى تجهز، فعليك الاطلاع على حلول أخرى. التمرينات 12.17 - 12.19 حاول إنجاز التمرينات التالية: التمرين 12.17: إعداد خادم وكيل معكوس Nginx أمام الواجهة الأمامية لتطبيق المهام سنحاول في هذا التمرين وضع خادم Nginx أمام الواجهتين الأمامية والخلفية لتطبيق المهام todo-app. سنبدأ بإنشاء ملف docker-compose جديد "todo-app/docker-compose.dev.yml" وملف تهيئة Nginx يحمل الاسم "todo-app/nginx.conf". todo-app ├── todo-frontend ├── todo-backend ├── nginx.conf └── docker-compose.dev.yml أضف الخدمتين nginx و todo-app/todo-frontend/dev.Dockerfile إلى الملف "todo-app/docker-compose.dev.yml". التمرين 12.18: إعداد خادم ليكون أمام الواجهة الخلفية لتطبيق المهام أضف الخدمة todo-backend إلى الملف "*todo-app/docker-compose.dev.yml" في وضع التطوير، ثم أضف مكانًا جديدًا للملف كي تُخدَّم الطلبات إلى العنوان "api/" عبر الخادم الوكيل إلى الواجهة الخلفية. قد يكون القالب التالي مناسبًا لإنجاز الأمر: server { listen 80; # (/) تُعالج الطلبات التي تبدأ بالمحرف location / { # نحتاج الأسطر الثلاثة التالية للتحميل المباشر للتغييرات proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; # http://localhost:3000 تُوجَّه الطلبات مباشرةً إلى العنوان proxy_pass http://localhost:3000; } # (/api/) تُعالج الطلبات التي تبدأ بالمسار location /api/ { ... } } للتوجيه proxy_pass ميزةٌ مهمة عندما تُضاف إليه الشرطة المائلة الزائدة "/"، وطالما أننا نستخدم المسار "api/" لتحديد المكان علمًا أن تطبيق الواجهة الخلفية سيجيب فقط على العنوان "/" أو "todos/" فلا بد من إزالة "api/" من الطلب. وبكلمات أخرى، حتى لو أرسل المتصفح الطلب GET إلى العنوان "api/todos/1/" نريد من الخادم Nginx أن ينقل الطلب بالوكالة إلى العنوان "todos/1/". لتنفيذ الأمر لا بد من إضافة شرطة مائلة "/" زائدة إلى العنوان في نهاية التوجيه proxy_pass. انتبه للموضوع جيدًا فقد تقضي ساعات في البحث عن حل لمشاكل سببها إهمال الشرطة الزائدة. وهذه إحدى المشاكل الشائعة: إليك هذه الصورة التوضيحة التي قد تنفع في توضيح الأمر عندما تواجهك المشاكل: التمرين 12.19: ربط الخدمتين todo-frontend و todo-backend سلّم في هذا التمرين بيئة التطوير بأكملها بما في ذلك تطبيقي Express و React وملفات Dockerfiles والملف "docker-compose.yml". تأكد بدايةً من عمل الواجهة الأمامية مع الواجهة الخلفية، وسيتطلب ذلك تغييرات في متغير البيئة REACT_APP_BACKEND_URL. وإذا كانت الواجهة جاهزةً للعمل خلال التمرين السابق يمكنك تجاوز هذه الخطوة. تأكد من أن بيئة التطوير تعمل الآن بكامل طاقتها، أي: أن تعمل جميع ميزات تطبيق المهام. عندما تغيّر الشيفرة المصدرية لا بُد أن تظهر نتيجة التغيرات مباشرةً في حال كان التغيير في الواجهة الأمامية وبإعادة تحميل التطبيق إن كان التغيير في الواجهة الخلفية. أدوات لمرحلة الإنتاج التعامل مع الحاويات ممتعٌ في مرحلة التطوير، لكن أفضل حالات الاستخدام ستكون في مرحلة الإنتاج، إذ توجد أدوات أكثر قدرة من docker-compose في تشغيل الحاويات في مرحلة الإنتاج. تسمح لنا أداة تنسيق الحاويات Kubernetes مثلًا بإدارة الحاويات ضمن مستوى جديد بالكامل، وتخفي تلك الأدوات عمومًا التجهيزات الفيزيائية المستخدمة مما يجعل المطورين أقل انشغالًا بأمور البنية التحتية. إن كنت ترغب في الاطلاع أكثر على الحاويات باستخدام Docker فعليك بالمنهاج DevOps with Docker، وكذلك المنهاج DevOps with Kubernetes لتتعلم تنسيق الحاويات باستخدام Kubernetes. التمرينات 12.20-12.22 حاول إنجاز التمرينات التالية: التمرين 12.20 أنشئ نسخة إنتاج من الملف "todo-app/docker-compose.yml" تضم كل الخدمات: Nginx و todo-backend و todo-frontend و MongoDB و Redis. استخدم الملف "Dockerfiles" بدلًا من "dev.Dockerfiles" وتأكد من تشغيل التطبيق في وضع الإنتاج. استخدم الهيكلية التالية في هذا التمرين: todo-app ├── todo-frontend ├── todo-backend ├── nginx.conf ├── docker-compose.dev.yml └── docker-compose.yml التمرين 12.21 أنشئ بيئة تطوير تعتمد على الحاويات مشابهة لما فعلنا، وذلك لأحد التطبيقات التي مرّت معك خلال منهاجنا أو التي أنشاتها في أوقات فراغك. اجعل هيكلية تطبيقك ضمن مستودع التسليم على النحو التالي: └── my-app ├── frontend | └── dev.Dockerfile ├── backend | └── dev.Dockerfile └── docker-compose.dev.yml التمرين 12.22 أنهِ القسم بإعداد نسخة إنتاج تعتمد على الحاويات خاصة بالتطبيق الذي اخترته. جعل هيكلية تطبيقك ضمن مستودع التسليم على النحو التالي: └── my-app ├── frontend | ├── dev.Dockerfile | └── Dockerfile ├── backend | └── dev.Dockerfile | └── Dockerfile ├── docker-compose.dev.yml └── docker-compose.yml ترجمة -وبتصرف- للفصل basics of orchestration من سلسلة Deep Dive Into Modern Web Development اقرأ أيضًا المقال السابق: بناء الصور وتهيئة بيئة العمل للتطبيقات المبنية ضمن الحاويات مدخل إلى الحاويات أبرز المفاهيم التي يجب عليك الإلمام بها عن الحاويات
  12. وصلنا في سلسلة مقالتنا إلى موضوع أكثر عمقًا، بعد أن خدشنا السطح الخارجي لأوبونتو، الذي هو أحد التوزيعات الشهيرة لنظام لينكس مفتوح المصدر. تأتي إنتاجية نظام التشغيل من كم ونوع التطبيقات التي يشغلها ويدعمها، وسهولة تثبيت هذه البرمجيات وإزالتها بأبسط طريقة ممكنة، دون أية مضاعفات تؤثر على استقرار النظام. وليس من السهل على القادمين الجدد إلى لينكس إدارة عملية تثبيت وإزالة التطبيقات، فليست جميعها بسهولة برمجيات ويندوز أو ماك التي يكفي في معظم اﻷحيان النقر المزدوج على ملف التثبيت ليقودك معالج التثبيت خطوةً خطوة خلال العملية، عن طريق واجهات رسومية يمكن استيعابها. مع ذلك، وكمستخدم مبتدئ، ومن منطلق أنك تستخدم حاسوبك لأغراض عامة؛ سنقدم لك مجموعةً مفيدةً من اﻷفكار والنصائح التي تساعدك في تثبيت معظم البرمجيات التي تحتاجها، دون أن تغوص في متاهات أوامر الطرفية Terminal، التي تتيح لك إمكانات واسعة في العمل مع النظام وفهمه في آن معًا، وإن كنت في مرحلة ما خلال مسيرتك، فستضطر إلى التعامل معها. تحديث النظام قبل كل شيء أوبونتو وكغيره من توزيعات لينكس، يُحدَّث باستمرار من جميع النواحي انطلاقًا من عملياته البنيوية وحتى البرمجيات التي ثُبِّتت عليه. ولهذا السبب وقبل أن تفكر في تثبيت أي تطبيق جديد، احرص أن يكوُن نظام التشغيل محدّثًا ومدعومًا بآخر إصدارات مكتباته واعتمادياته. في واقع اﻷمر، يُحدّث أوبونتو نفسه بنفسه، فهو مُعدٌّ افتراضيًا للتحديث التلقائي؛ إذ يتصل النظام بخوادم مخصصة وموثوقة للحصول على آخر إصدارات الحزم Packages التي تضم معلومات التحديث أو الموارد الجديدة، ثم يفك هذه الحزم ويثبت محتواها. مع ذلك، لابد من الاطلاع على بعض التفاصيل المهمة التي يمكن الوصول إليها من خلال تطبيق "البرمجيات والتحديثات". تطبيق "البرمجيات والتحديثات" لننتقل إذًا إلى قائمة التطبيقات ونفتح هذا التطبيق. يعرض التطبيق مجموعةً من النوافذ الفرعية، وسنلقي نظرةً على ما نحتاجه منها حاليًا: برمجيات Ubuntu: يحدد هذا الخيار ما يمكن تثبيته من برمجيات خاصة لدعم نظام أوبونتو من خلال اﻹنترنت، إذ يعطيك خيار تثبيت برمجيات حرة ومفتوحة المصدر مصدرها الرئيسي هو شركة كانونيكال (الشركة المطورة لنظام التشغيل أوبونتو) وتدعى البرمجيات الرئيسية Main؛ كما يعطيك خيار تثبيت برمجيات حرة ومفتوحة المصدر من شركاء تدعمها كانونيكال (تُدعى برمجيات عامة Universe)، وخيار البحث عن تعريفات للعتاد الصلب مملوكة من قبل مطوريها، وخيار تثبيت برمجيات مقيدة ومملوكة لأصحابها (تُدعى برمجيات مختلفة المصادر Multiverse). قد تختار بعض هذه اﻷنواع من البرمجيات أو جميعها بما يلائمك وننصحك بترك هذه اﻹعداد كما هو. برمجيات أخرى: لتنزيل برمجيات وتعريفات تطوّرها كانونيكال لحساب شُركائها مع شيفرتها المصدرية. التحديثات: يتيح لك التطبيق مجموعةً من الخيارات للتحقق من وجود تحديثات وهي: التسجيل في قائمة التحديثات subscribe to: تتيح لك اﻹشتراك بكل التحديثات all updates أو بالتحديثات اﻷمنية والتحديثات المزكّاة security and recommended updates، أو التحديثات اﻷمنية فقط. ويُنصح المبتدئ هنا بترك الخيار اﻹفتراضي. فحص التحديثات تلقائيًا: قد تختار فحص التحديثات يوميًا أو أسبوعًا أو شهريًا، وقد لا تختار الفحص التلقائي أبدًا. لذا يُنصح هنا باختيار الفحص التلقائي اﻷسبوعي، لأنها فترة زمنية معقولة لوجود تحديثات أمنية، وهذا أمر حيوي جدًا. عند وجود تحديثات أمنية: يُنصح هنا بخيار "اعرضها فورًا"، فهذا الخيار أكثر مرونةً، ويبقيك على اطلاع بما سيجري تثبيته؛ إذ يعرض لك النظام من خلال تطبيق آخر يُدعى "مُحدث البرمجيات" نوع التحديثات المتوفرة، ويترك لك الخيار بتثبيتها أو لا. قد يجد الكثيرون أن خيار "نزّلها وثبتها تلقائيًا" هو اﻷنسب كمبتدئ -وهذا صحيح للوهلة اﻷولى-، لكنك قد تشعر بالصدمة إن كنت تحاول في وقت ما تثبيت برنامج ضروري ولا تلاحظ أي تقدم في عملية تثبيته! عند توفر تحديثات أخرى: ننصحك أيضًا بخيار "اعرضها فورًا" للأسباب السابقة نفسها. لن نتحدث عن باقي النوافذ الفرعية للتطبيق كي لا ندخلك في دوّامات لست مستعدًا لها حاليًا، وسنكتفي بهذا القدر من الشرح، فهذا ما تحتاجه حاليًا. تطبيق "محدث البرمجيات" يبحث هذا التطبيق عن آخر التحديثات التي تتعلق بالبرمجيات وتحديثات النظام والتحديثات اﻷمنية، وذلك وفقًا لخياراتك التي اعتمدها في تطبيق "البرمجيات والتحديثات"، كما يعرض لك كل جديد تلقائيًا، أو يمكنك فتحه إن أردت من قائمة التطبيقات. يصنف هذا التطبيق التحديثات المتوفرة كما يلي: قاعدة Ubuntu: وتضم التحديثات الجديدة على نواة النظام. تحديثات أمنية. تحديثات برمجية. يمكن أن تختار ما تشاء منها لتثبيته حسب اﻷولوية التي تراها، ووفقًا لوقتك وانشغالك؛ لكن يُنصح هنا بتثبيتها تباعًا إن لم يكن فورًا. انقر على زر "ثبت اﻵن" لبدأ عملية تثبيت التحديثات، أو "تذكير لاحقًا" للتثبيت في وقت لاحق. تثبيت تطبيقات "سناب" من خلال تطبيق "برمجيات" من جنوم سناب هو اﻹسم الذي يُطلق على حزمة برمجية تضم كل ما يتطلبه تطبيق معين من ملفات وموارد واعتماديات، ليعمل باستقلالية وبشكل منفصل عن ملفات النظام واعتمادياته. لقد صُممت حزم سناب من قِبل شركة كانونيكال نفسها التي صممت أوبونتو، وكان الهدف منها أن تعمل التطبيقات على كل توزيعات لينكس بسلاسة، ودون أن تُضطر إلى مشاركة بيانات أو ملفات مع تطبيقات أخرى مخصصة للعمل على توزيعات مختلفة. يُحمِّل المطوّرون وأصحاب الشأن (وفق آلية محددة) تطبيقات سناب إلى ما يُعرف بمخزن سناب أو Snap-Store بما يشابه ما يجري على متجر غوغل بلاي أو ماك ستور. ولكي تستعرض هذه التطبيقات وتثبتها، ستجد نفسك أمام خيارين: اﻷول هو تعليمات سطر اﻷوامر (الطرفية)، أو الثاني هو خيار الواجهات الرسومية التي يفضّلها المستخدمون العاديون أو حديثو العهد. وفي كلتا الحالتين يعمل تطبيق سناب-دي أو سناب دايمون snapd على ضمان تنزيل وتثبيت وتحديث تطبيقات سناب تلقائيًا. لا أنوي إطلاقًا الخوض في تفاصيل الطرفية وتعليماتها في هذا المقال، لذا سأتوجه مباشرةً إلى الواجهات الرسومية التي يتوفر منها الكثير، والتي تختلف اﻷراء واﻷهواء في استحسانها واستهجانها. مع ذلك، وطالما أننا نعمل على سطح مكتب جنوم سنستخدم الواجهة الرسومية الخاصة به وتُدعى "برمجيات" في النسخة العربية. نظرة على "برمجيات" يرتبط هذا البرنامج تلقائيًا بمخزن سناب (وبغيره من المصادر الموثوقة أيَضًا)، ويعرض لك من خلال واجهة رسومية واضحة وسهلة الاستخدامات. أغلب التطبيقات المتوفرة مصنفة بطريقة يسهل معها البحث، كما تساعدك الواجهة أيضًا على تثبيت وإزالة التطبيقات واستعراضها؛ مع اﻹشارة إلى التطبيقات التي تتوفر لها تحديثات (مع أن تطبيقات سناب تُحدّث تلقائيًا). انقر على زر "أظهر التطبيقات" في شريط التطبيقات، ثم انقر على تطبيق "برمجيات". إن طُبَّقت أية تحديثات مؤخرًا على نظامك، فستظهر لك الشاشة التالية: تخبرك هذه الشاشة بأن التطبيق يتواصل مع مخازن التطبيقات لتنزيل قوائم بأسماء التطبيقات المتوفرة حاليًا ومعلومات عنها. وعند اﻹنتهاء من تنزيل هذه المعلومات، ستظهر لك نافذة التطبيق كما في اللقطة التالية: لنبدأ أولًا باستكشاف نافذة التطبيق قبل أن ننتقل إلى موضوع تثبيت وإزالة التطبيقات. يحتوي شريط المهام على ثلاثة أزرار تَعرض ثلاثة نوافذ فرعية: استكشاف Explore: وهي النافذة التي تُعرض افتراضيًا. تُصنِّف النافذة التطبيقات الموجودة في المخازن إلى فئات مختلفة مثل "الصوتيات والمرئيات" و"اﻷلعاب" و"أدوات التطوير" وغيرها، وبالنقر على أية فئة، ستعرض لك النافذة مجموعة التطبيقات التي تنتمي إلى هذه الفئة، كما يمكنك ترتيبها وفقًا للتقييم أو وفقًا للاسم؛ كما تعرض النافذة بعض التطبيقات التي يزكّيها المحررون، إذ يرونها مناسبة ومفيدة للمستخدم. المنصَّبة: تعرض هذه النافذة جميع التطبيقات المثبتة على حاسوبك سواءٌ التطبيقات التي ثبتها بنفسك، أو التي ثُبتت افتراضيًا مع النظام؛ باﻹضافة إلى تطبيقات النظام. ستجد إلى جوار كل تطبيق الزر "أزل" ﻹزالة التطبيق، كما ستجد أسفل الزر حجم هذا التطبيق. التحديثات: تعرض هذه النافذة التطبيقات التي تتوفر لها تحديثات جديدة إن لم تكن هنالك تطبيقات تحتاج إلى تحديث تبلغك النافذة أن برامجك محدّثة. تطبيق عملي: تثبيت تطبيق معالجة الصور "جيمب" من جنوم كمثال عن تثبيت البرمجيات افتح تطبيق "برمجيات"، ثم اختر فئة "الرسوميات والتصوير"؛ وابحث بين التطبيقات المتوفرة عن تطبيق"جنو لمعالجة الصور"، أو انقر على زر البحث في أقصى يمين شريط مهام النافذة، واكتب "gimp" ليبحث عنه التطبيق؛ عندها ستظهر النتائج التي تتطابق مع معايير البحث، ومن بينها تطبيق "جيمب". انقر على أيقونة التطبيق لتنتقل إلى نافذة التثبيت التي تقدم معلومات عن عمل التطبيق ورخصة استخدامه ومصمميه وبعض المراجعات والتقييمات التي وضعها المستخدمون. لتثبيت البرنامج، انقر على زر "نصّب". قد تستغرق العملية وقتًا طويلًا أو قصيرًا وفقًا لحجم التطبيق ولسرعة اتصالك باﻹنترنت، والضغط على خادم التثبيت ومشغولية نظامك (فقد يكون مشغولًا في تنصيب أو تحديث بعض البرمجيات). على أية حال، اترك لتطبيق "برمجيات" الوقت الكافي ﻹنجاز الأمر. يخبرك "برمجيات" عند انتهاء التنزيل والتثبيت بأن البرنامج قد ثُبِّت بنجاح على حاسوبك، وستظهر أيقونته ضمن قائمة التطبيقات. تثبيت التطبيقات المجمّعة في حزم ديبيان Dep. لا بد من اﻹشارة إلى وجود الكثير من التطبيقات التي ينتجها مطوّرون مستقلون أو شركات معروفة مجمّعة أو محزّمةً بأرشيفات تدعى حزم ديبيان (نسبةً لنظام التشغيل لينكس ديبيان الذي يتفرع عنه أوبونتو)؛ إذ تقدم هذه الشركات تطبيقاتها على مواقعها الخاصة ويمكنك عندها تنزيل هذه الحزمة وتثبيت التطبيق. تُعدُّ حزم ديبيان أقل حجمًا من مثيلاتها في سناب، إلا أنها أقل أمانًا، وقد ترتبط بتطبيقات أخرى أو بموارد واعتماديات للنظام. تظهر حزم ديبيان على شكل مجلد مضغوط وردي اللون ينتهي اسم الحزمة باللاحقة dep.. ولتثبيت هذه الحزمة، انقر نقرًا مزدوجًا عليه، وهذا كل ما في اﻷمر. يهتم تطبيق "البرمجيات" بأمر تثبيت هذا التطبيق بالطريقة الملائمة، فعلى الرغم من ارتباط هذا التطبيق بمخازن سناب لتنزيل وتثبيت هذا النوع من التطبيقات، إلا أنه قادر أيضًا على الارتباط بتطبيق "تنصيب البرمجيات" لإدارة حزم ديبان وتنصيبها بطريقة مشابهة لتطبيقات سناب. تطبيق عملي: تثبيت برنامج "فيجوال ستديو كود" من حزمة ديبيان ابحث بكل بساطة مستخدمًا أي محرك بحث عن "visual studio code" وهو تطبيق من شركة مايكروسوفت لتحرير كم هائل من ملفات الشيفرة للكثير من اللغات ويُعد محررًا ممتازًا لك كمبتدئ أيضًا إن أردت البدء بتعلم البرمجة. تعرض لقطة الشاشة التالية لمتصفح فايرفوكس صفحة تنزيل البرنامج: لاحظ أن التطبيق متوفر على الموقع بصيغة dep. لتوزيعات ديبيان وأوبونتو. انقر على الرابط، وسيبدأ المتصفح بتنزيل حزمة التطبيق. عند اكتمال التطبيق، انقر نقرًا مزدوجًا على اﻷرشيف، وستظهر لك النافذة التالية المرتبطة بتطبيق "البرمجيات": انقر زر "نصّب" وانتظر اكتمال عملية التثبيت. حزم أخرى هل سأصادف حزمًا أخرى يفهمها أوبونتو على أنها تطبيقات ويثبتها من خلال تطبيق "البرمجيات"؟ في الواقع نعم، وسنتكلم بإيجاز عن أحدها، وهي حزم "فلات-باك"، اختصارًا للامتداد flatpak. أو اﻹصدار اﻷحدث الذي يضم تعليمات وصفية أكثر عن التطبيق flatpakref. تمتاز هذه الحزم بأنها محتواة في حاوية خاصة بها وتثبّت في معظم توزيعات لينكس دون الحاجة إلى شيئ خارج إطار الحاوية، فهي مستقلة بذاتها مثل تطبيقات سناب. ويمكنك الحصول على هذه الحزم من مخزن فلات-هب flathub، حيث يرفع المطوّرون تطبيقاتهم إليه، كما يمكن أن تجدها على سناب-ستور أيضًا وقد تكون العديد من التطبيقات التي تثبتها من خلال تطبيق "البرمجيات" هي حزم "فلات-باك" في الواقع. على أية حال إن حصلت على التطبيق الذي تريد تثبيته مجمّعًا في أرشيف فلات-باك، فاﻷمر يسير. انقر نقرًا مزدوجًا على الحزمة، وسيتكفل تطبيق "البرمجيات" بكل شيء كما في حزم "ديبيان". خلاصة يساعدك تطبيق "برمجيات" على البحث عن العديد من التطبيقات الموجودة في مخازن مختلفة وموثوقة، إذ يعرض لك وصفًا عن عملها ورخصة استخدامها والجهة التي طوّرتها وتقييم المستخدمين لها. إن لم تجد التطبيق الذي تبحث عنه في قوائم البرمجيات التي يقدمها تطبيق "البرمجيات"، فعليك البحث عنه ضمن مواقع اﻹنترنت، وبالطبع قد تكون العملية محفوفةً بالمخاطر. بمجرد أن تجد الموقع المناسب لتنزيل التطبيق المطلوب وتتأكد من سلامة الموقع والحزمة، حاول أن تنزّل النسخة الخاصة بنظام أوبونتو. وإن تعّذر اﻷمر أو رأيته غامضًا، فابحث عن حزم بإحدى الامتدادات أو اللواحق التالية: dep. flatpak. flatpakref. نزّل هذه الحزمة ثم انقر نقرًا مزدوجًا عليها لتبدأ عملية التثبيت. وهكذا نكون قد قدمنا في مجموعة المقالات هذه الموجهة إلى القادمين الجدد إلى عالم لينكس، شرحًا مكثفًا ومبسطًا لمعظم النواحي التي يحتاجها مستثمر نظام أوبونتو حتى يألف بيئة العمل ويزيد من إنتاجيته. اقرأ أيضًا المقال السابق: إعدادات أوبونتو 20.04: التجهيزات والإعدادات الإقليمية والشمولية إعدادات أوبونتو 20.04: التطبيقات وإدارة المستخدمين تعرّف على سطح مكتب أوبونتو 20.04 التعامل مع المجلدات والملفات في أوبونتو 20.04 تغيير اللغة في نظام لينكس أوبنتو إلى العربية
  13. يصف هذا المقال ما يجري عند استعراض صفحة ويب على حاسوبك أو هاتفك المحمول بصورة مبسّطة، فقد لا تجد هذا الأمر بدايةً أمرًا أساسيًا لكي تكون قادرًا على كتابة شيفرة ويب، لكنك ستجني بسرعة فائدة معرفتك خفايا العملية. الخوادم والعملاء تُدعى الحواسب التي تتواصل على ويب بخوادم servers وعملاء clients، وإليك توضيحًا مبسّطًا عن طريقة تفاعلها: العميل: يُعرَّف تقليديًا بأنه جهاز متصل بويب مثل حاسوب عبر شبكة لاسلكية أو هاتف محمول عبر شبكة خلوية، بالإضافة إلى برنامجيات على هذه الأجهزة قادرة على الوصول إلى ويب مثل متصفحات ويب مثل كروم أو فايرفوكس عادةً. الخادم: هو حاسوب يخزِّن صفحات ومواقع وتطبيقات ويب. حيث يزوّد الخادم الجهاز العميل بنسخة عن صفحة ويب عندما يريد الوصول إليها ليتمكن من عرضها على متصفح المستخدِم. تفاصيل أكثر لن تكتمل القصة بمفهومَي الخادم والوكيل التي عرضناها قبل قليل، فهنالك الكثير من الأجزاء الأخرى التي سنناقشها تاليًا. دعونا نتخيل الآن أنّ الويب هي طريق يقع العميل في طرفه الأول وليكن بيتك مثلًا ويقع الخادم في طرفه الآخر وليكن متجرًا تشتري منه حاجياتك على سبيل المثال. عليك أن تتعرف على ما يلي بالإضافة إلى الخادم والعميل: الاتصال بالإنترنت: الذي يسمح لك بإرسال واستقبال البيانات من وإلى ويب، فهو تقريبًا مثل الشارع الذي يربط بيتك بالمتجر. بروتوكول TCP/IP: يُعَدّ بروتوكول التحكم بالنقل Transmission Control Protocol وبروتوكول الإنترنت Internet Protocol بروتوكلَي اتصال يحددان آلية نقل البيانات عبر الإنترنت، إذ يشابه هذان البروتوكولان وسيلة التنقل التي تتخذها لتنظيم رحلتك من منزلك إلى المتجر وشراء حاجياتك، فقد تكون في مثالنا سيارة أو دراجة أو أيّ شيء مماثل. خادم أسماء النطاقات Domain Name System أو DNS اختصارًا: يمكن تشبيهه بدفتر عناوين لمواقع ويب، فعندما تكتب عنوان موقع ويب في متصفحك، يبحث خادم DNS عن عنوان بروتوكول الإنترنت IP address الموافق للموقع قبل إحضاره، إذ يحتاج المتصفح لمعرفة الخادم الذي يستضيف الموقع المطلوب ليرسل رسائل HTTP إلى الوجهة الصحيحة، ويشبه الأمر في مثالنا البحث عن عنوان المتجر. بروتوكول HTTP: وهو بروتوكول نقل النص التشعبي Hypertext Transfer Protocol ويُعَدّ بروتوكولًا تطبيقيًا يُوصِّف لغةً بين الخادم والعميل ليتواصلا من خلاله، أي الأمر مشابه أيضًا للغة المحكية التي تتواصل بها لطلب حاجياتك من المتجر. الملفات المكوّنة للموقع: يتألف الموقع من ملفات مختلفة بصورة مشابهة للبضائع المختلفة التي تحتاجها من المتجر، وتأتي هذه الملفات ضمن صنفين أساسيين: ملفات شيفرة Code files: تُبنى مواقع ويب بصورة أساسية من ملفات HTML و CSS وجافاسكربت، كما ستتعرف لاحقًا على ملفات أخرى تخدم تقانات مختلفة. ملفات مساعدة أو الأصول Assets: تشمل كل الأشياء التي يتكوّن منها الموقع مثل الصور والموسيقى والفيديو والمستندات النصية وغيرها. ما الذي يحدث عند طلب موقع ويب؟ إليك ما يحدث عندما تكتب عنوان موقع في شريط متصفحك، وهو أمر مماثل لانطلاقك نحو المتجر: يتوجه المتصفح إلى خادم DNS ويجد العنوان الحقيقي للخادم الذي يستضيف هذا الموقع، أي مثل إيجاد العنوان المفصّل للمتجر. يرسل المتصفح طلب HTTP إلى الخادم يسأله فيها إرسال نسخة عن موقع الويب إلى العميل، أي عندما تذهب إلى المتجر وتطلب حاجياتك، إذ تُرسل الطلبات وغيرها من البيانات بين العميل والخادم عبر اتصال الإنترنت من خلال بروتوكول TCP/IP. سيرسل الخادم إذا وافق على طلب العميل رسالةً تحمل العدد 200 وتعني تمامًا أنه بإمكانك الاطلاع على هذا الموقع، إذ يبدأ الخادم بعدها بإرسال ملفات الموقع إلى المتصفح على أساس سلسلة من القطع تُدعى رزم البيانات، أي عندما يبدأ المشرف على المتجر بإعطائك البضائع التي طلبتها ثم تنقلها إلى منزلك. يُجمِّع المتصفح حزم البيانات ضمن صفحة مكتملة ويعرضها لك مثل وصول حاجياتك كاملة إلى منزلك واستخدامها. الترتيب الذي يستخدمه المتصفح في تفسير الملفات عندما تطلب المتصفحات من الخوادم ملفات HTML، فستشير هذه الملفات من خلال العناصر <link> إلى ملفات CSS خارجية، ومن خلال العناصر <script> إلى ملفات جافاسكربت الخارجية، فمن المهم إذًا معرفة الترتيب الذي تفسر به المتصفحات ملفات موقع عندما تحمّله: يفسّر المتصفح ملف HTML أولًا، وبالتالي سيميّز العناصر <link> التي تشير إلى ملفات CSS الخارجية، كما يميّز العناصر <script> التي تشير إلى ملفات الشيفرة. يرسل المتصفح أثناء تفسير ملف HTML طلبات إلى الخوادم التي قد تستضيف ملفات CSS أو ملفات الشيفرة التي تشير إليها العناصر السابقة، ثم يفسّر هذه الملفات. يولّد المتصفح شجرة DOM مقيمة في الذاكرة انطلاقًا من ملف HTML الذي فسّره ومن ثم شجرة CSSOM بعد تفسير ملف CSS ثم يصرِّف وينفذ شيفرة جافاسكربت التي فسّرها. عندما يبني المتصفح شجرة DOM ويطبّق التنسيقات استنادًا إلى شجرة CSSOM وينفّذ شيفرة جافاسكربت، تُعرض الصفحة على الشاشة بعملية تُدعى الرسم paint ليراها المستخدِم ويكون قادرًا على التفاعل معها. توضيح عن خادم أسماء النطاقات DNS من الصعب تذكُّر عناوين ويب الحقيقية لأنها ليست عبارات نصية واضحة، وإنما هي أعداد خاصة تبدو مشابهةً للسلسلة 63.245.215.20، إذ تُدعى هذه السلسلة بعنوان بروتوكول الإنترنت IP address ويمثل موقعًا فريدًا على ويب، وطالما أنه من الصعب تذكر هذه العناوين، فقد اختُرعت خوادم أسماء النطاقات، وهي خوادم خاصة تربط عنوان موقع ويب الذي تدخله في شريط المتصفح مثل "hsoub.com" بعنوان بروتوكول الإنترنت الحقيقي له. يمكن أيضًا الوصول إلى مواقع ويب من خلال عناوين بروتوكول الإنترنت، لكن عليك أن تحصل أولًا على هذا العنوان عن طريق بعض الأدوات مثل IP Checker. توضيح عن رزم البيانات استُخدم المصطلح رزم packets لوصف هيئة البيانات التي يرسلها الخادم إلى العميل، فما الذي نعنيه بالرزم هنا؟ عندما تُرسل البيانات عبر الويب فإنها تُرسَل عبر آلاف الأجزاء الصغيرة وذلك لأسباب كثيرة، فقد تُهمل هذه البيانات أو تتشوه أثناء نقلها ومن الأسهل استبدال قطعة صغيرة إذا حدث ذلك، إضافةً إلى هذا الأمر، فقد توجّه الرزم إلى مسارات مختلفة، مما يجعل تبادلها أسرع ويسمح لعدد كبير من المستخدِمين تنزيل الموقع نفسه في الوقت عينه، بينما إذا أُرسِل الموقع دفعةً واحدةً، فلن يتمكن سوى مستخدِم واحد من تنزيل الموقع كل مرة، مما يجعل الويب غير فعالة وغير مسليَة. ترجمة -وبتصرف- للمقال How the web works. اقرأ أيضًا كيف تعمل شبكة الإنترنت كيفية التعامل مع الويب المدخل الشامل لتعلم تطوير الويب
  14. تُعَدّ جافاسكربت JavaScript لغة برمجة تزيد من القدرة التفاعلية لمواقع ويب، وتشاهد ذلك مثلًا في الألعاب وفي مظاهر استجابة الصفحات عند نقر الأزرار أو عند إدخال البيانات إلى النماذج والاستمارات الإلكترونية، أو من خلال التغيير الديناميكي لتنسيق الصفحة أو عبر الرسوم المتحركة وغيرها، إذ سيساعدك هذا المقال لتبدأ استخدام جافاسكربت ويعرّفك أكثر بإمكانيات هذه اللغة. تعرف على جافاسكربت تُعَدّ جافاسكربت لغة برمجة قوية تزيد من القدرة التفاعلية لمواقع ويب، وقد ابتكرها برنارد آيتش Brendan Eich، وهو مؤسس مشارك لمشروع موزيللا ومؤسسة موزيللا وشركة موزيللا، كما تُعَدّ جافاسكربت لغةً قريبةً من المبرمجين المبتدئين، إذ ستتمكن مع الممارسة والخبرة من إنشاء ألعاب ثنائية وثلاثية الألعاب وبناء تطبيقات عصرية مرتبطة بقواعد بيانات وغير ذلك الكثير، وصحيح أنّ هذه اللغة مدمجة مع المتصفحات، لكنها مرنة، فقد بنى المطورون الكثير من الأدوات انطلاقًا من جافاسكربت، مقدِّمين كمًا واسعًا من القدرات الوظيفية بأقل مجهود ممكن، ونذكر منها : واجهات برمجية للتطبيقات API مدمجة بالمتصفحات لتزويدها بوظائف إضافية مثل الإنشاء التلقائي لشيفرة HTML وضبط تنسيقات CSS أو التقاط وتعديل الفيديوهات المصورة عن طريق كاميرا ويب أو توليد رسوميات ثلاثية الأبعاد ومقاطع صوتية. واجهات برمجية لتطبيقات صممها طرف ثالث تسمح للمطورين دمج وظائف يقدمها مزوّدو محتوى مثل تويتر وفيس بوك ضمن مواقع أخرى. أطر عمل ومكتبات صممها طرف ثالث ويمكن تطبيقها على صفحات HTML لتسريع بناء المواقع والتطبيقات. لا يغطي مجال هذا المقال تفاصيل الاختلاف بين الأدوات التي بُنيت وتبنى باستخدام جافاسكربت واللغة نفسها، لذلك يمكن دائمًا البحث عن تفاصيل مثل هذه عبر الإنترنت، وسيقدِّم لك القسم التالي من المقال بعض مزايا جافاسكربت البنيوية، كما سيمنحك إمكانية تجريب بعض مزايا الواجهات البرمجية الخاصة بالمتصفحات أيضًا. كتابة أول برنامج في جافاسكربت تُعَدّ جافاسكربت من أكثر التقنيات الحديثة شعبيةً، وستُدخِل مواقعك مع تقدم مهارتك فيها أبعادًا جديدةً من القوة والإبداع، لكن التآلف مع هذه اللغة أصعب من الاعتياد على العمل مع HTML و CSS، فقد تبدأ بالقليل ثم تنمو قدراتك تدريجيًا، ولنبدأ بتفحّص الطريقة التي سنضيف فيها جافاسكربت إلى صفحتك لتنفيذ برنامج "!Hello world"، وهو برنامج معياري لتقديم لغة برمجة إلى المستخدِم لأوّل مرة. تنبيه: إذا لم تكن لسبب ما متابعًا للأفكار التي تحدثنا عنها في مقالات سابقة، فيمكنك تنزيل الشيفرة التجريبية للمثال واعتماده على أساس نقطة انطلاق. انتقل إلى المجلد الذي يضم موقعك التجريبي وانشئ ضمنه مجلدًا باسم scripts، ثم انشئ ضمن الأخير ملفًا نصيًا ثم احفظه بالاسم main.js. افتح الملف index.html واكتب قبل نهاية العنصر <body> سطر الشيفرة التالية: <script src="scripts/main.js"></script> ينفِّذ هذا العنصر ما ينفذه تمامًا العنصر <link> عندما أدرج ملف تنسيق CSS، إذ يطبِّق هذا العنصر شيفرة جافاسكربت الموجودة في الملف على عناصر HTML في الصفحة بالإضافة إلى تنسيقات CCS أو أيّ شيء آخر. أضف الشيفرة التالية إلى الملف main.js: const myHeading = document.querySelector('h1'); myHeading.textContent = 'Hello world!'; تأكد من حفظ التغيرات على الملفَين index.html وmain.js، ثم حمّل الملف index.html مجددًا في المتصفح الذي سيعرض صفحةً شبيهةً بالتالي: ملاحظة: يعود سبب وضع العنصر <script> في نهاية ملف HTML إلى طريقة القراءة المتسلسلة للشيفرة وفق ترتيب ظهور العناصر التي يتّبعها المتصفح. إذا حمّل المتصفح ملف جافاسكربت أولًا قبل عناصر HTML التي يُفترض أن يؤثر فيها، فقد تقع بعض المشاكل، لذلك من الأفضل تحميله بعد تحميل العناصر بوضع العنصر في نهاية صفحة HTML، فهذا سيحل المشكلة. ما الذي حدث؟ لقد تغيّر عنوان الصفحة إلى "!Hello world" باستخدام جافاسكربت، إذ نُفِّذت العملية عبر استدعاء الدالة ()querySelector التي التقطت مرجعًا إلى عنصر العنوان <h1> وخزّنته في المتغير myHeading، أي الأمر مشابه لما فعلناه باستخدام محددِّات CSS، فإذا أردت تطبيق شيء ما على عنصر، فلا بد من تحديده أولًا. بعد ذلك أُسندت إلى الخاصية textContent العائدة للمتغير myHeading والتي تمثل محتوى العنوان القيمة الجديدة "!Hello world". ملاحظة: تُعَدّ كلتا الميزتين المستخدَمين في هذا المثال جزءًا من الواجهة البرمجية لشجرة DOM التي تمتلك القدرة على التعامل مع مستند HTML. دورة تطوير التطبيقات باستخدام لغة JavaScript تعلم البرمجة بلغة جافا سكريبت انطلاقًا من أبسط المفاهيم وحتى بناء تطبيقات حقيقية. اشترك الآن أساسيات لغة جافاسكربت سنشرح لك فيما يأتي بعض الميزات البنيوية لجافاسكربت لتفهم طريقة عملها بصورة أفضل، ومن الجدير بالذكر أنّ هذه الميزات مشتركة بين كل لغات البرمجة، فإذا أتقنت هذه الأساسيات، فستكون قد وضعت حجر الأساس لكتابة شيفرات بلغات أخرى. تنبيه: حاول أن تُدخل أسطر الشيفرة التجريبية التي تتعلمها في هذا المقال ضمن طرفية جافاسكربت JavaScript console المضمنة داخل المتصفح، ولمزيد من المعلومات عن هذه الطرفية راجع مقال أدوات مطوري ويب المدمجة في المتصفحات. المتغيرات تُعدّ المتغيرات حاويات لتخزين القيم، إذ تبدأ القصة عندما تُصرِّح عن متحول باستخدام التعليمة var (مع أنها غير محبّذة، ستجد التفاصيل لاحقًا) أو التعليمة let يتبعها الاسم الذي تختاره للمتغير: let myVariable; تشير الفاصلة المنقوطة في نهاية السطر إلى نهاية الجملة البرمجية، وتحتاجها فقط عندما تريد الفصل بين العبارات البرمجية في السطر ذاته، لكن وضع فاصلة منقوطة في نهاية كل سطر هي عادة برمجية جيدة، وهنالك عدة قواعد أخرى عن وجوب استخدام الفاصلة المنقوطة وعدم وجوب ذلك، كما يمكنك تسمية المتغيِّر أيّ اسم تقريبًا مع بعض القيود، إذ يمكن الاطلاع على توثيق المتغيرات في موسوعة حسوب، وإذا لم تكن متأكدًا مما كتبت، فتستطيع استخدام بعض الخدمات على الإنترنت للتحقق من صحة تسمية المتغيرات، كما ينبغي الانتباه إلى أنّ جافاسكربت حساسة لحالة الأحرف، فالمتغير myVariable يختلف تمامًا عن myvariable، لذلك تحقق من مشاكل مثل هذه عندما تواجهك الأخطاء. يمكنك إسناد قيمة إلى المتغير بعد التصريح عنه: myVariable = 'Bob'; يمكنك تنفيذ خطوتَي التصريح والإسناد في سطر واحد: let myVariable = 'Bob'; استدع المتغير وحسب لتحصل على القيمة: myVariable; يمكن تغيير قيمة المتغير لاحقًا في مواضع أخرى في الشيفرة: let myVariable = 'Bob'; myVariable = 'Steve'; يمكن أن تحمل المتغيرات قيمًا من أنواع مختلفة: String: يمثل مجموعةً من محارف تمثل سلسلةً نصيةً، ولابد من وضع السلسلة بين إشارتَي إقتباس مفردتين كما يلي: let myVariable ='Bob'; Number: يمثل عددًا، ولا يُوضَع ضمن شارتي تنصيص. let myVariable =10; Boolean: ويمثل أحد القيمين المنطقيتين: صحيح true أو خاطئ false، وهاتين الكلمتين من الكلمات الخاصة المحجوزة في لغة جافاسكربت ولا حاجة لوضعهما بين إشارتي تنصيص: let myVariable = true; Array: يمثل مايُدعى بالمصفوفة، وهي بنية لتخزين عدة قيم تحت مرجع واحد: let myVariable = [1,'bob',10,'mad'] // يمكن الإشارة إلى كل عنصر من عناصر المصفوفة كما يلي myVariable[0]; // 1 تعرض أولى قيم المصفوفة وهي myVariable[3]; // 'mad' تعرض القيمة الأخيرة Object: قد يحمل أي نوع من القيم، فكل شيء في جافاسكربت هو كائن Object يمكن أن يُخزَّن في متغير: let myVariable = document.querySelector('h1'); // الشيفرة نفسها التي استخدمناها سابقًا لِمَ نحتاج المتغيِّرات إذًا؟ لأنها ضروية لتنفيذ كل ما له معنى في البرمجة، فالمتغيرات مخازن القيم، ولن تحصل على أية أفعال ديناميكية مثل تخصيص رسالة ترحيب أو تغيير الصورة المعروضة إذا لم تتغير القيم. التعليقات تُعَدّ التعليقات Comments مقتطفات نصيةً تُضاف إلى الشيفرة ويتجاهلها المتصفح، كما يمكن استخدام التعليقات في جافاسكربت الأسباب نفسها التي تستخدمها في CSS، ويشار إلى نص على أنه تعليق كما يلي: /* Everything in between is a comment. */ لكي يمتد التعليق على عدة أسطر، أو كما يلي: // This is a comment إذا كان التعليق على سطر واحد. العوامل يُعَدّ العامل Operator رمزًا رياضيًا يعطي نتيجةً استنادًا إلى قيمتين أو متغيرين، وإليك بعض العوامل الأبسط التي تُستخدَم في جافاسكربت مع بعض الأمثلة، وحاول تجريبها على طرفية جافاسكربت: الجمع: رمزه +، ويضيف عددين معًا أو يضم نصين إلى بعضهما: 9+6; //15 'hello' + 'world'; // helloworld الطرح والضرب والقسمة: رموزها بالترتيب -، *، /، وعملها مشابه تمامًا لعملها الرياضي: 4-5; //-1 3*3;//9 10/2;//5 الإسناد: رمزه =، ويسند قيمة إلى متغير: let myVar = 3; المساواة: رمزها ===، وتختبر تساوي قيمتين وتعيد نتيجةً منطقيةً؛ إما صحيح true أو خاطئ false : let myVar =3; myVar ===4; //fals تعيد النفي: رمزه !، ويعيد القيمة المنطقية المعاكسة لما يتقدمها، إذ تُغيِّر true إلى false وبالعكس: let myVar=3; !(myVar===4); //true تعيد عدم المساواة: رمزها ‎!==‎، وتختبر عدم تساوي قيمتين وتعيد النتيجة المنطقية المناسبة: let myVar=4; myVar==!3; //true تعيد هناك الكثير من العوامل الأخرى في جافاسكربت، لكننا سنكتفي الآن بما ذكرناه. ملاحظة: قد يقود دمج أنواع مختلفة من البيانات عند إجراء العمليات الحسابية إلى أخطاء، لذا فكن حذِرًا بالإشارة إلى متغيراتك لتحصل على النتيجة المتوقعة، فإذا نفَّذت العملية '35' +'25'، فستحصل على 2535 وهذا ما قد لا تتوقعه، لأن إشارات التنصيص المفردة تجعل ما داخلها نصوصًا لا أعدادًا، بينما إذا نفَّذت العملية على الصورة 35 + 25، فستحصل على نتيجة الجمع الصحيحة. العبارات الشرطية تُعَدّ العبارات الشرطية بُنًى تُستخدَم لاختبار تحقق شرط معيَن نتيجته true أو false، ومن أكثر الصيغ الشرطية استخدامًا هي العبارة if...else، وإليك مثالًا كما يلي: let iceCream = 'chocolate'; if(iceCream === 'chocolate') { alert('Yay, I love chocolate ice cream!'); } else { alert('Awwww, but chocolate is my favorite...'); } تختبر البنية الشرطية العبارة التي تقع ضمن (...)if، إذ تستخدِم البنية في الشيفرة السابقة عامل المساواة للموازنة بين قيمتي المتغّير iceCream والنص chocolate والتحقق من تساويهما، فإذا أعادت الموازنة القيمة true، فستُنفَّذ الكتلة الأولى من الشيفرة التي تلي تعليمة الشرط (...)if، وإلا فستُنفَّذ الكتلة الثانية التي تقع بعد العبارة else. الدوال تُعَدّ الدوال طريقةً لتنظيم الشيفرة التي ترغب في استخدامها مرارًا، إذ يمكنك أن تُعرِّف مثلًا مجموعةً من أسطر الشيفرة على أساس دالة يجري تنفيذها عندما تستدعيها باسمها في أي مكان من الشيفرة، إذ تملك هذه الطريقة فعاليةً كبيرةً في منع تكرار كتابة الشيفرة نفسها كلما احتجنا لها، ولقد رأينا في الشيفرات السابقة نماذجًا لدوال مثل: let myVariable = document.querySelector('h1');//هي دالة querySelector وكذلك: alert('hello!'); إنّ الدالتَين document.querySelector و alert مدمجتان مع المتصفح، فإذا رأيت شيئًا يشبه المتغير متبوعًا بقوسين ()، فهو دالة على الأغلب، كما تأخذ الدوال أشياء تُدعى وسائط arguments، وهي بيانات تحتاجها لتنفيذ وظائفها، إذ تُوضع الوسائط داخل قوسَي الدالة وتفصل بينها فاصلة ,. تعرض الدالة alert على سبيل المثال نافذةً منبثقةً ضمن المتصفح، فلا بد من تزويدها بنص على هيئة وسيط لكي تعرضه، كما يمكنك أيضًا تعريف دالة خاصة بك، إذ سننشئ في المثال التالي دالةً تأخذ وسيطَين عددين ثم تعيد ناتج جدائهما: function multiply(num1,num2) { let result = num1 * num2; return result; } حاول تنفيذ ذلك في الطرفية، ثم اختبر الدالة بتغيير قيم الوسيطَين كما يلي: multiply(4, 7); multiply(20, 20); multiply(0.5, 3); ملاحظة: تخبر التعليمة return في نهاية الدالة أن تعيد قيمةً هي نتيجة تنفيذ الدالة غالبًا -مثل result في المثال السابق- لكي تستطيع استخدامها، وهذا الأمر ضروري لأن المتغيرات التي تُعرَّف داخل الدالة لا يمكن استخدامها خارج الدالة وهذا ما يُعرَف بمجال رؤية المتغير variable scoop. الأحداث لابد من معالجة الأحداث التي تقع في الصفحة لتظهر تفاعلية الصفحة، فالأحداث events هي بنى برمجية تُنصِت إلى النشاطات التي تجري في المتصفح وتستجيب لها بتنفيذ شيفرة محددة، ومن أكثر الأحداث وضوحًا هو حدث النقر على الفأرة والذي يقع عندما تنقر بالفأرة على شيء ما ضمن الصفحة، ولشرح الفكرة، أدخِل الشيفرة التالية في طرفية جافاسكربت ثم انقر على الصفحة التي يعرضها المتصفح: document.querySelector('html').addEventListener('click', function() { alert('Ouch! Stop poking me!'); }); هناك طرق عدة لربط معالِج الحدث event handler بالعنصر، فقد اخترنا في الشيفرة السابقة العنصر <html>، ومن ثم استدعينا معه الدالة addEventListener()‎ مع تحديد اسم الحدث (في حالتنا 'click' النقر) المراد الإنصات له مع تحديد الدالة المراد تنفيذها عند وقوع ذلك الحدث. لاحظ الشيفرة التالية التي تشبه تمامًا الشيفرة السابقة في العمل: document.querySelector('html').onclick = function() { alert('Ouch! Stop poking me!'); } أسندنا فيها الخاصية onclick التي تمثل معالج حدث النقر (الذي يكون اسم الحدث مسبوقًا بكلمة on) إلى دالة دون اسم anonymous تضم الشيفرة التي نريد تنفيذها عندما يقع حدث النقر. أيضًا الشيفرة السابقة والتي تسبقها مماثل للشيفرة التالية: let myHTML = document.querySelector('html'); myHTML.addEventListener('click', function() { alert('Ouch! Stop poking me!'); }); لكنه أقصر. الدالة السابقة التي ممرناها إلى الدالة addEventListener()‎ تدعى دالة مجهولة لعدم امتلاكها اسمًا، وهنالك طريقة أخرى لكتابة دوال مجهولة تدعى الدوال السهمية التي تستعمل الصيغة ‎() =>‎ بدلًا من function ()‎، انظر مثلًا: document.querySelector('html').addEventListener('click', () => { alert('Ouch! Stop poking me!'); }); تطوير المثال التجريبي الذي نعمل عليه لنضِف بعض الميزات الجديدة إلى صفحة الويب التي نطوّرها باستخدام ما تعلمناه عن جافاسكربت. احذف بداية محتوى الملف main.js واحفظ الملف فارغًا كي لا تتعارض الشيفرات الجديدة مع تلك التي كتبناها سابقًا. تغيير للصورة سنتعلم استخدام جافاسكربت وميزات واجهة DOM البرمجية لتبديل الصورة مرةً أو مرتين عند النقر عليها. اختر صورةً جديدةً لاستخدامها والأفضل أن يكون لها قياس الصورة الأولى نفسه. احفظ الصورة في المجلد images باسم firefox2.png. أضف شيفرة جافاسكربت التالية إلى الملف main.js: let myImage = document.querySelector('img'); myImage.onclick = function() { let mySrc = myImage.getAttribute('src'); if(mySrc === 'images/firefox-icon.png') { myImage.setAttribute('src','images/firefox2.png'); } else { myImage.setAttribute('src','images/firefox-icon.png'); } } احفظ جميع التغيرات وحمّل الملف index.html في المتصفح وانقر على الصورة، إذ يجب أن تتغير الصورة المعروضة عند النقر. إليك ما حدث: لقد خزنت مرجعًا إلى العنصر <img> في المتغير myImage، ثم أسندت قيمة الخاصية onclick للمتغير والتي تمثل معالِج حدث النقر إلى دالة دون اسم لكي يُنفِّذ محتواها كلما نقرت على الصورة، وما تفعله الشيفرة هو استخلاص قيمة السمة src لعنصر الصورة واستخدم بنية شرطية للتحقق أن قيمتها تساوي مسار الصورة الأصلية، فإذا كانت كذلك، فستُغيِّر الشيفرة قيمة السمة src للعنصر <img> إلى مسار الصورة الثانية لكي يعرضها المتصفح، وإذا لم تكن كذلك، فهذا يعني أنّ قيمة السمة src قد تغيرت سابقًا وستعيدها الشيفرة إلى قيمتها الأصلية لتُعرض الصورة الأساسية وهكذا. إضافة رسالة ترحيب خاصة لنغيّر الآن عنوان الصفحة كي يعرض رسالة ترحيب خاصة بالمستخدِم عند زيارته للصورة وستبقى هذه الرسالة، ولإن غادر الزائر الصفحة وعاد مجددًا ستظهر الرسالة، لأننا سنخزنها باستخدام الواجهة البرمجية للتخزين على المتصفح، وسنقدم أيضًا خيارًا لتغيير المستخدِم، وبالتالي تغيير رسالة الترحيب. أضف السطر التالي في الملف index.html فقط قبل العنصر <script>: <button>Change user</button> أي أسفل الملف. ضع الشيفرة التالية في نهاية الملف main.js كما هي تمامًا، إذ تُخزِّن هذه الشيفرة مرجعًا إلى الزر الجديد ومرجعًا إلى العنوان داخل متغيرَين: let myButton = document.querySelector('button'); let myHeading = document.querySelector('h1'); أضف الدالة التالية لتخصيص رسالة الترحيب، إذ لن تفعل الدالة شيئًا بالطبع حتى اللحظة لكنها ستفعل قريبًا: function setUserName() { let myName = prompt('Please enter your name.'); localStorage.setItem('name', myName); myHeading.textContent = 'Mozilla is cool, ' + myName; } تحتوي الدالة ()setUserName على الدالة ()prompt التي تعرض مربع حوار على شاشة المتصفح بصورة مشابهة للدالة ()alert لكنها تنتظر من المستخدِم إدخال قيمة لكي تخزنها بعد أن ينقر الزر موافق OK. نطلب في هذه الحالة من المستخدِم إدخال اسمه ثم تستدعي الشيفرة الواجهة البرمجية localStorage التي تسمح بتخزين البيانات في ذاكرة المتصفح للوصول إليها لاحقًا، كما نستخدِم الدالة ()setItem لإنشاء وتخزين عنصر بيانات يُدعى name ثم إسناد قيمته إلى المتغير myName الذي يحوي القيمة التي يدخلها المستخدِم للاسم، ونضيف أخيرًا قيمة المتغير myName إلى محتوى العنوان ثم يُسند النص الناتح إلى الخاصية textContent ليظهر الاسم الذي أدخلناه على أساس جزء من العنوان. أضف الكتلة الشرطية if ... else التالية: if(!localStorage.getItem('name')) { setUserName(); } else { let storedName = localStorage.getItem('name'); myHeading.textContent = 'Mozilla is cool, ' + storedName; } يمكننا استدعاء هذه الشيفرة عند بداية تحميل الصفحة كونها شيفرة تهيئة للمحتوى، إذ يستخدِم السطر الأول منها عامل النفي المنطقي ! للتحقق من عدم وجود عنصر البيانات name ضمن مخازن الذاكرة LocalStorage، فإذا لم يجده، فسيستدعي الدالة ()setUserName لإنشاءه؛ أما إذا كان موجودًا -أي أنّ المستخدِم أدخله في أثناء زيارته الأولى-، فسنعيد قيمته باستخدام الدالة ()getItem ثم نضبط قيمة محتوى العنوان ليصبح النص الأصلي إضافة إلى اسم المستخدِم كما فعلنا ضمن الدالة ()setUserName. أضف معالِج الحدث onclick التالي إلى الزر لكي تُستدعى الدالة ()setUserName عند النقر عليه، وبالتالي سيتمكن المستخدِم من تغيير الاسم عند النقر على هذا الزر: myButton.onclick = function() { setUserName(); } ظهور القيمة null إذا نقرت زر إلغاء cancel بدل زر موافق ok أثناء ظهور مربع الحوار، فسيظهر لك عنوان الصفحة "Mozilla is cool, null"، ويعود السبب في ذلك إلى عدم إسناد قيمة إلى المتغير myName وبالتالي سيأخذ القيمة null، وهي قيمة خاصة في جافاسكربت تشير إلى غياب قيمة مطلوبة؛ أما إذا نقرت زر موافق ok دون إدخال اسم، فستكون النتيجة ",Mozilla is cool" وهذا أمر واضح، ولتفادي هذه المشاكل يمكنك التحقق من وجود الاسم فعلًا، لذلك سنعدِّل شيفرة الدالة ()setUserName كما يلي: function setUserName() { let myName = prompt('Please enter your name.'); if(!myName) { setUserName(); } else { localStorage.setItem('name', myName); myHeading.textContent = 'Mozilla is cool, ' + myName; } } يعني هذا إعادة استدعاء الدالة ()setUserName من جديد إذا لم يكن للمتغير قيمة؛ أما إذا كان له قيمة، فستُخزَّن ضمن localStorage وتُضاف إلى العنوان. خلاصة إذا اتبعت التوجيهات التي أشرنا إليها خلال اطلاعك على هذا المقال، فستبدو صفحة الويب التي نبنيها بالشكل التالي تقريبًا: إذا لم تجد عملك صحيحًا، فيمكنك دائمًا موازنة ما فعلته مع النسخة الجاهزة على جيت-هاب. ترجمة -وبتصرف- للمقال JavaScript basics. اقرأ أيضًا توثيق لغة JavaScript العربي تعلم البرمجة ما هي جافاسكربت الدليل السريع إلى لغة البرمجة جافاسكريبت JavaScript سلسلة دليل تعلم جافاسكربت
  15. نتابع في هذا المقال ما بدأناه في ضبط إعدادات النظام بما يلائم حاجات المستخدم، وسنناقش المواضيع التي تختص بإدارة التجهيزات المثبتة، مثل التجهيزات الصوتية وشاشات العرض؛ كما نعرّج على مواضيع الإعدادات اللغوية والإقليمية وإعدادات الشمولية (الوصول السهل accessibility) التي تحسّتن من تجربة المستخدمين ذوي القدرات الخاصة، وذوي المشاكل البصرية والسمعية . إعدادات التجهيزات وتتضمن مجموعةً من الخيارات التي تتحكم بإعدادات بعض التجهيزات، مثل الصوت والطاقة والفأرة وغيرها. إعدادات الصوت للولوج إلى هذه اﻹعدادات، انقر على خيار "الصوت" في الشريط الجانبي لنافذة اﻹعدادات، وسيعرض لك التطبيق النافذة التالية: تضم الشاشة السابقة الأقسام التالية: شدة صوت نظام: يحدد مستوى الصوت في حاسوبك، ويمكنك ضبط نفس اﻹعداد من شريط المهام الرئيسي لسطح المكتب. تجدر اﻹشارة إلى وجود آلية لتعزيز الصوت، أي إيصاله إلى مستويات أعلى من 100% (يتعلق اﻷمر بالشريحة الصوتية المدمجة في نظامك وإمكاناتها)، وذلك بالنقر على زالقة خيار "تضخيم زائد over _ amplification". مستويات الصوت: يمكِّنك هذا الخيار من ضبط شدة الصوت لبعض التطبيقات مثل أصوات النظام التي يصدرها عند النقر على أيقونة، أو فتح وإغلاق نافذة، أو التنبيهات الصوتية أو لبعض المشغلات الصوتية (لاحظ وجود زالقة للتحكم بمستوى صوت تطبيق VLC؛ مما يدل على أن هذا المشغل الصوتي مثبّت على جهازك، ويعمل في لحظة التقاط صورة للشاشة). المخرج: عادةً ما يختار النظام تلقائيًا المنفذ الذي يخرج منه الصوت إن دعمت الشريحة الصوتية في حاسوبك أكثر من مخرج، وذلك عن طريق التقاط الإشارة ناتجة عن توصيلك مكبر الصوت أو السماعة في هذا المنفذ. يمكنك بالطبع اختيار أي منفذ لإخراج الصوت من قائمة "جهاز إخراج"، كما يتيح لك الزر "اختبار" المجاور للقائمة أن تختبر عمل هذا المخرج الصوتي. المدخل: ويتيح لك اختيار المنفذ الذي تدخل منه الإشارات الصوتية إلى حاسوبك لتسجيلها أو عرضها عندما تصل مايكروفون أو أي تجهيزة مشابهة بهذا المنفذ. صوت التنبيه: وذلك لاختيار نغمة التنبيه الصوتي أو نغمة الصوت المرافق لظهور تنبيه على الشاشة. إعدادات الطاقة تضم هذه اﻹعدادات بعض الخيارات التي تساعد على تخفيف استهلاك الطاقة قدر المستطاع سواءً أثناء العمل، أو عند التوقف عن العمل مؤقتًا؛ كما تتيح لك التحكم بما سيفعله حاسوبك عند الضغط على زر التشغيل. خيارات حفظ الطاقة تعتيم الشاشة blank screen: وهي الفترة الزمنية التي ينتظرها النظام بعد توقفك عن العمل لإخفاء شاشة سطح المكتب. يمكنك اختيار مدة تتراوح بين دقيقة و"أبدًًا" إن لم تكن تريد إخفاء سطح المكتب. واي فاي: تشغيل أو إطفاء محوّل الشبكة اللاسلكية لتوفير الطاقة إن لم تكن تستخدم الشبكة. بلوتوث: تشغيل أو إطفاء محوّل بلوتوث لتوفير الطاقة إن لم تكن تستخدمه. خيارات تعليق النظام والضغط على زر تشغيل الحاسب نعرض فيما يلي خيارات تعليق النظام والضغط على زر تشغيل الحاسب: التعليق التلقائي للنظام automatic suspend: وذلك لتعليق عمل النظام بعد فترة من الزمن إن لم تكن تستخدم الحاسوب. هذا الخيار معطّل افتراضيًا، لكن بإمكانك تفعيله ثم اختيار الفترة الزمنية التي تناسبك ما بين 15 دقيقة وساعتين. الضغط على زر التشغيل power button action: يمكنك أن تطفئ الجهاز، وهو الخيار اﻹفتراضي أو تعلق عمله أو أن يتجاهل الحاسوب أمر الضغط على زر التشغيل عند اختيار "لا تفعل شيئًا". إعدادات شاشة العرض تتيح لك هذه اﻹعدادات ضبط خواص جميع أجهزة العرض المتصلة مع حاسوبك. ضبط الشاشات الاتجاه: لضبط سياق العرض وفقًا للشاشة المستخدمة، فقد يكون العرض طوليًا أو عرضيًا. الميز أو دقة العرض: يختار النظام اﻹعدادت اﻷنسب تلقائيًا، لكن بإمكانك اختيار دقة العرض التي تراها مناسبة (وهي عدد البكسلات العرضية وعدد البكسلات الطولانية ونسبة الطول إلى العرض). ضبط اﻷبعاد الكسري fractional scaling: ويقصد به تكبير أو تصغير تفاصيل سطح المكتب باستخدام معاملات تكبير أو تصغير عشرية بدلًا من الصحيحة. لا تهتم كثيرًا للأمر إن لم تدرك تمامًا ما المقصود بذلك، لكن تفعيل هذا الخيار قد يزيد من دقة التفاصيل وملائمتها لشاشتك ككل، وقد يستهلك ذلك في المقابل قدرًا أكبر من الطاقة، كما قد يُخفض من حدة تمايز اﻷلوان. الشاشات الليلية للوصول إلى نافذة هذا الخيار، انقر على زر "اﻹضاءة الليلية" في شريط مهام النافذة. يغير هذا الخيار من شدة سطوع الشاشة ليلًا، ويجعل اﻷوان أكثر دفئًا (اقترابًا من اللون اﻷحمر)، مما يعطي راحةً أكبر للعين، ويخفف اﻹجهاد. يضبط النظام تلقائيًا الفترة الزمنية التي يحل فيها الظلام في منطقتك وفقًا للإعدادات المحلية له (عندما تحدد البلد وحزمة التوقيت)، كما يمكنك وضع جدول زمني لساعات اﻹضاءة الليلية بما يناسبك. يتيح لك النظام أيضًا خيار ضبط مقدار دفئ اﻷلوان لتحصل على اﻹضاءة التي تريحك. إعدادات أجهزة الدخل وتتيح ضبط بعض الميزات المتعلقة بالفأرة ولوحة المفاتيح والتطبيقات التي ستفتح تلقائيًا محتوى وسائط التخزين الخارجية. الفأرة ولوحة اللمس الزر اﻷساسي: انقر على الخيار "يسار" لتستخدم زر الفأرة الأيسر في تنفيذ اﻷوامر، أو "يمين" لتستخدم الزر الأيمن. وفي كلتا الحالتين يتحول الزر الآخر إلى زر قوائم (إظهار قائمة إن وجدت). سرعة الفأرة: عندما تحرك الزالقة إلى اليسار، فأنت تزيد بذلك من سرعة مؤشر الفأرة أثناء حركته على الشاشة وتبطئ حركة المؤشر إن حركت الزالقة يمينًا. اختصارات لوحة المفاتيح تعرض لك هذه النافذة جميع اختصارات لوحة المفاتيح مرتبة حسب طبيعة عملها، مثل اختصارات التنقل، أو اختصارات فتح وتشغيل تطبيقات وغيرها. تساعدك هذه الاختصارات في تنفيذ ما تريده بسرعة وتبقى مسألة اختيار الاختصارات التي تستخدمها في عملك أمرًا خاصًا بك وحدك. يقدم لك نظام التشغيل أوبونتو إمكانية تغييرالاختصار الافتراضي أو تعطيله أو إضافة اختصارات أخرى. مثال تطبيقي: يُعد تطبيق "المرقاب" شديد اﻷهمية في معرفة كل التطبيقات التي تعمل حاليًا وحجم الذاكرة الذي يستخدمه كلًا منها، باﻹضافة إلى مراقبة موارد النظام ومنظومة الملفات. سنعين اختصارًا لتشغيل هذا التطبيق: افتح التطبيق بالطريقة التقليدية: من قائمة التطبيقات، اختر "أدوات" ثم "مرقاب النظام". عندما يفتح التطبيق، ابحث عن العملية التي لها أيقونة مشابهة لأيقونة التطبيق، وستجد أن اسمها "gnome-system-monitor". احفظ هذا الاسم. أغلق التطبيق، ثم افتح اﻹعدادات، ومنها إلى خيار "اختصارات لوحة المفاتيح". انقر على الزر (+) في آخر القائمة. ليظهر لك مربع حوار يطلب من إدخال بعض المعلومات ضع في حقل "الاسم" أي اسم تريده لاختصارك، ثم ضع في حقل "اﻷمر" الاسم الذي حفظته في الخطوة الأولى كما هو تمامًا دون أية أخطاء. اضغط على زر حدد اختصارًا لتظهر لك النافذة التالية: انقر على الأزرار التي تريدها أن تشكل الاختصار معًا، مثل Ctrl + Alt + M، وسيكون الاختصار جاهزًا للاستخدام دومًا. التشغيل التلقائي لمحتوى الوسائط الخارجية انقر على خيار "الوسائط المنفصلة" وستظهر النافذة التالية: تقدم النافذة مجموعةً من الخيارات التي ينفذها النظام عند اكتشاف وجود وسيط تخزين خارجي يضم محتوى معين: صوتي، أو فيديو، أو صور، أو برمجيات. بالنقر على عنصر القائمة المجاور لكل نوع من المحتوى، ستظهر مجموعة من الخيارات في مقدمتها البرامج التي يزكّيها النظام لتشغيل هذا النوع من الوسائط، كما يعطيك حرية اختيار البرنامج الذي تريده إن كان مثبتًا على جهاز من خلال اختيار الأمر"تطبيق آخر". يمكنك أيضًا تجاهل الموضوع باختيار اﻷمر "لا تفعل شيئًا"، وإن أردت أن تقرر ما الذي ستفعله في الوقت المناسب، فاختراﻷمر "اسأل ما الذي يجب فعله"، أو بإمكانك فتح وسيط التخزين من خلال تطبيق "الملفات" باختيار اﻷمر "افتح المجلّد" بكل بساطة. إن لم يكن وسيط التخزين مدرجًا ضمن الوسائط المعروضة، فانقر على الخيار "وسائط أخرى"، وسيعرض لك مربع الحوار التالي: اختر نوع الوسيط، ثم اختر اﻹجراء المطلوب اتخاذه عند اكتشافه، وسيتذكر النظام ما اخترته. إن لم تشأ أن يسألك النظام عما يجب فعله عند اكتشاف وسيط تخزين جديد وأن لا يشغل تلقائيًا أية برامج، ففعّل الخيار "لا تسأل أو تبدأ البرامج عند إدخال الوسائط". اﻹعدادات اللغوية واﻹقليمية تتيح هذه اﻹعدادات ضبط الكثير من النقاط الهامة التي تتعلق بلغة نظام التشغيل ولغات لوحة المفاتيح والتنسيقات العامة كالتاريخ والوقت والعملة وغيرها. المنطقة واللغة عند النقر على خيار "المنطقة واللغة" في الشريط الجانبي لتطبيق "اﻹعدادات"، ستظهر نافذة تضم الخيارات التالية: اللغة: وتدل على لغة نظام التشغيل ولغة قوائمه ونوافذه. لتغييرها، انقر على هذا الخيار، ثم اختر إحدى اللغات المثبتة بالفعل على جهازك. ستتطلب العملية إعادة تشغيل النظام عند اﻹنتهاء. النسق: وتعرض تنسيق الوقت والتاريخ والعملة وجملة القياسات المستخدمة في البلد المحدد. لتغيير النسق، انقر على هذا الخيار، واختر نسق البلد الذي تنتمي إليه، ثم انقر على الزر "تمّ". تتطلب التغييرات إعادة تشغيل الحاسوب قبل أن تُطبق. مصادر اﻹدخال: وذلك لاختيار لوحات مفاتيح مخصصة لكتابة لغات معينة. لاحظ إمكانية الكتابة باللغة العربية واﻹنكليزية، لأن تخطيطات لوحة المفاتيح لكل منها مهيئة ومفعلة. إن أردت إضافة لغة إدخال جديدة، فانقر على زر (+) أسفل آخر تخطيط، وسيظهر لك مربع حوار لاختيار لغة جديدة. اختر اللغة، ثم انقر الزر "أضف". إدارة اللغات المثبتة manage installed languages: يفتح هذا الخيار تطبيق "دعم اللغات"، ويعرض اللغات المثبتة حسب اﻷفضلية. اللغة اﻷولى في القائمة هي لغة النظام الحالية، وتكوّن مثبتةً مع كامل اعتمادياتها ومكتباتها، لكن إن لم يجد النظام مقابلًا لتسمية أو زر أو خيار بهذه اللغة، فسيستخدم مقابلها من اللغة التالية في القائمة. عند اختيار ترتيب اللغات بالشكل الذي تريد، انقر على زر "طبّق على مستوى النظام"، وسيُنفذ النظام اﻷمر عند تسجيل الخروج، ثم تسجيله مجددًا. إن أردت أن تستخدم لغةً أخرى لتكون لغةً أساسيةً للنظام، أو رأيت أن دعم اللغة العربية غير مكتمل؛ فانقر على زر "ثبّت/احذف اللغات" الذي يتيح لك قائمةً بمجموعة اللغات التي يدعمها النظام كليًا أو جزئيًا. انقر على اللغة التي تريد تثبيتها، ثم انقر على الزر "طبّق". يبحث النظام عن اﻹعتماديات المثبتة فعلًا ويقرر ما إذا كانت هناك أية نواقص كي يستدعي عندها "محدّث البرمجيات" لتزيل وتثبيت ما يلزم تلقائيًا ودون تدخل منك. اﻹتاحة وشمولية الوصول يقصد بمصطلح اﻹتاحة أو الشمولية accessibility، تمكين أي مستخدم من العمل على نظام التشغيل بأفضل تجربة ممكنة إيًا كانت لغته أو إمكاناته الجسدية (سليم، أو لديه مشاكل بصرية، أو لديه مشاكل سمعية… إلخ). يتيح لك نظام أوبونتو بنسخته 20.04 طيفًا واسعًا من الخيارات التي تسهّل على ذوي القدرات الخاصة التعامل مع النظام، وسنلقي نظرةً سريعةً عليها في هذا القسم. انقر على خيار "اﻹتاحة" في الشريط الجانبي لنيافة تطبيق "اﻹعدادات" وستظهر لك الخيارات التالية: التباين العالي: يغيرّر النظام عند تفعيل هذا الخيار سمة سطح المكتب ليعرض اﻷيقونات والقوائم وأشرطة المهام والنوافذ باللونين اﻷسود واﻷبيض بتدرجات لونية ضئيلة لمساعد ضعيفي البصر على التمييز. نص كبير: تظهر جميع الكتابات بحجم كبير لتسهيل القراءة. حجم المؤشر: لاختيار حجم مؤشر الفأرة الظاهر على الشاشة بما يلائم المستخدم. التقريب: بالنقر على هذا الخيار ستظهر لك النافذة التالية: التكبير: ويحدد مقدار تكبير ما يُعرض على الشاشة ابتداءً من 1 وحتى 5 أضعاف. يتبع مؤشر الفأرة: تلحق المكبرة بمؤشر الفأرة لتكبير المنطقة التي يصل إليها. جزء من الشاشة: تبقى المكبرة في النصف العلوي أو السفلي أو اﻷيمن أو اﻷيسر من الشاشة أو تملؤها كلها. في النافذة الفرعية التالية "محاور الهدف"، يمكنك تفعيل ميزة ظهور محورين متقاطعين يتمركزان حول مؤشر الفأرة لتحديد موضعها مع إمكانية ضبط طول هذين المحورين وسماكتهما ولونهما. قارئ الشاشة: ينطق قارئ الشاشة المدمج مع النظام أي محتوى نصي يقع عليه مؤشر الفأرة، كما ينطق نوع العنصر الذي يحوي هذا المحتوى (زر، أو نافذة، أو رسالة) وموقعه. لا يُنصح به في النسخة العربية لأنه سيشوش المكفوف أكثر مما يساعده. هنالك خيارات أخرى متعددة يتيحها النظام للمستخدمين ذوي القدرات الخاصة وحتى العاديين أحيانًا، نترك للقارئ مهمة استكشافها والتعرف عليها كونها تشرح نفسها بنفسها غالبًا. خلاصة تعرفنا في المقالات الثلاثة التي تحمل عنوان "إعدادات أوبونتو" على طريقة ضبط الإعدادات العامة لنظام تشغيل أوبونتو من وجهة نظر المستخدم أو المستثمر العادي للنظام بما يحقق له الراحة و الفعالية في العمل وعرّجنا على مواضيع هامة تتعلق بإنشاء وضبط شبكات الاتصال بأنواعها وضبط إعدادات التطبيقات، من ثم إدارة التجهيزات. وهكذا سنكون قد خطونا خطوة بسيطة أخرى في عالم نظام التشغيل لينكس وتوزيعاته وأصبحنا جاهزين للانتقال إلى موضوع مهم جديد وهو تثبيت التطبيقات التي يحتاجها المستخدم عبر واجهات رسومية واضحة وسهلة الاستخدام. اقرأ أيضًا المقال السابق: إعدادات أوبونتو 20.04: التطبيقات وإدارة المستخدمين تعرّف على سطح مكتب أوبونتو 20.04 التعامل مع المجلدات والملفات في أوبونتو 20.04 تغيير اللغة في نظام لينكس أوبنتو إلى العربية
  16. استخدمنا في جزئية سابقة من هذه السلسلة صورتين مختلفتين هما "ubuntu" و "node" ونفّذنا بعض الأعمال يدويًا لتشغيل تطبيق "Hello, World". ستساعدنا الأدوات والأوامر التي تعلمناها سابقًا في هذه الجزئية من السلسلة، إذ نتعلم فيه بناء الصور وتهيئة بيئة عمل التطبيق. سنبدأ ببناء واجهة خلفية نمطية باستخدام Express/Node.js ثم نبني عليها مستخدمين خدمات أخرى مثل قواعد بيانات MongoDB. ملفات Dockerfile بإمكاننا إنشاء صورة جديدة تتضمن التطبيق "!Hello, World" بدلًا من تعديل الحاوية بنسخ ملفات جديدة إليها، وتساعدنا في ذلك أداة تُدعى Dockerfile، وهي ملفٌ نصي بسيط يحتوي كل التعليمات الخاصة بإنشاء صورة. سنبدأ إذًا بإنشاء مثال عن Dockerfile من تطبيق "!Hello, World". أنشئ مجلدًا جديدًا على جهازك ثم أنشئ ضمنه الملف "Dockerfile " إن لم تكن قد فعلت ذلك مسبقًا. ولنضع كذلك الملف "index.js" الذي يضم الشيفرة ('!console.log('Hello, World إلى جواره. ستبدو هيكلية المجلد على النحو التالي: ├── index.js └── Dockerfile سنخبر الصورة من خلال "Dockerfile" بثلاثة أمور: استخدم node:16 أساسًا للصورة. ضع الملف "index.js" ضمن الصورة، كي لا نُضطر إلى نسخه يديويًا إلى الحاوية. استخدم node لتنفيذ شيفرة الملف "index.js" عندما نشغّل الحاوية من الصورة. توضع هذه النقاط الثلاث ضمن ملف "Dockerfile" وأفضل مكان لإنشاء هذا الملف هو جذر المشروع، وسيبدو هذا الملف على النحو التالي: FROM node:16 WORKDIR /usr/src/app COPY ./index.js ./index.js CMD node index.js FROM: تخبر هذه التعليمة برنامج دوكر Docker أنّ أساس الصورة هو node:16. COPY: تنسخ هذه التعليمة الملف index.js من الجهاز المضيف إلى ملف بنفس الاسم ضمن الصورة. CMD: تخبر هذه التعليمة البرنامج ما يجب أن يحدث عند تنفيذ الأمر docker run. وهذه التعليمة هي تعليمة تنفيذ افتراضية يمكن استبدالها بالمعامل الذي يُعطي بعد اسم الصورة. اكتب الأمر docker run --help إن نسيت. WORKDIR: وضعت هذه التعليمة للتأكد من أننا لن نفعل شيئًا يتداخل مع محتوى الحاوية. إذ سيضمن أنّ كل التعليمات التي ستليه ستكون ضمن المجلد "usr/src/app/" الذي يُعد مجلد العمل في هذه الحالة. فإن لم يكن هذا المجلد موجودًا في الصورة، سيُنشأ تدريجيًا. لم نحدد مجلد عمل WORKDIR، فقد نجازف بتغيير ملفات هامة بطريق الخطأ. لو تحققت من الجذر / للصورة بتنفيذ الأمر docker run node:16 ls ستجد العديد من المجلدات والملفات في هذه الصورة. نستطيع الآن استخدام الأمر docker build لبناء الصورة بناءً على ملف Dockerfile، لكننا سنضيف الراية t- إلى هذا الأمر كي تساعدنا في إعادة تسمية الصورة: $ docker build -t fs-hello-world . [+] Building 3.9s (8/8) FINISHED ... ستكون نتيجة تنفيذ الأمر هي: "docker please build with tag fs-hello-world the Dockerfile in this directory" والتي تشير إلى بناء صورة بالاسم "fs-hello-world" بالاعتماد على ملف Dockerfile الموجود في المجلد. يمكنك الإشارة إلى أي ملف Dockerfile لكن في حالتنا البسيطة يكفي وضع . للإشارة إلى هذا الملف ضمن المجلد لهذا انتهى الأمر بالنقطة. يمكنك تشغيل الحاوية الآن باستخدام الأمر docker run fs-hello-world ويمكن نقل الصورة أو تنزيلها أو حذفها فهي في طبيعتها ملفات. إذ يمكنك تشكيل قائمة بالصور التي يضمها حاسوبك باستخدام الأمر docker image ls أو حذف الصورة ‍‍docker image rm. اطلع على بقية الأوامر المتاحة بتنفيذ أمر المساعدة docker image --help. صور أكثر فائدة ينبغي أن يكون نقل خادم Express إلى حاوية ببساطة نقل تطبيق "!Hello, World"، إذ يكمن الاختلاف الوحيد بين الحالتين في وجود ملفات أكثر في حالة الخادم، لكن ستغدو الأمور أبسط بوجود التعليمة COPY. لنحذف الآن الملف "index.js" وننشئ خادم Express باستخدام express-generator الذي يساعدنا على بناء هيكلية بسيطة للتطبيق. $ npx express-generator ... install dependencies: $ npm install run the app: $ DEBUG=playground:* npm start لنشغل التطبيق الآن كي نرى ما فعلنا، وانتبه أن أمر التشغيل قد يختلف في جهازك، فالمجلد في المثال السابق يُدعى "playground". $ npm install $ DEBUG=playground:* npm start playground:server Listening on port 3000 +0ms يمكنك الانتقال الآن إلى العنوان "http://localhost:3000" حيث يعمل التطبيق. إن ضم الملفات في حاويات هي عملية سهلة نوعًا ما بناءً على ما واجهناه حتى الآن: استخدم الأساس node. اضبط مجلد العمل كي لا يتداخل عملك مع بقية محتويات الصورة. انسخ كل ملفات المجلد إلى الصورة. ابدأ بتنفيذ الأمر DEBUG=playground:* npm start بعد الأمر CMD. لنضع ملف Dockerfile التالي في جذر المشروع: FROM node:16 WORKDIR /usr/src/app COPY . . CMD DEBUG=playground:* npm start سنبني الآن الصورة انطلاقًا من ملف باستخدام الأمر: docker build -t express-server . وسنشغلها باستخدام الأمر: docker run -p 3123:3000 express-server تبلّغ الراية p- دوكر بضرورة فتح منفذ الجهاز المضيف وتوجيهه إلى منفذ للحاوية ولهذا الأمر الصيغة التالية: p host-port:application-port- $ docker run -p 3123:3000 express-server > playground@0.0.0 start > node ./bin/www Tue, 29 Jun 2021 10:55:10 GMT playground:server Listening on port 3000 إن لم يفلح الأمر، تجاوز الفقرة التالية، فهناك تفسير لعدم نجاح الأمر حتى لو اتبعت الخطوات السابقة تمامًا. سيبدأ التطبيق عمله الآن، لهذا سنختبره بإرسال الطلب GET إلى العنوان "/http://localhost:3123". بالنسبة لإيقاف الخادم فهو أمر عصيبٌ حاليًا، لهذا افتح نافذة أخرى للطرفية ونفذ الأمر docker kill لإيقاف التطبيق؛ إذ يرسل هذا الأمر الإشارة SIGKILL إلى التطبيق ويجبره على الإنهاء. ستحتاج إلى اسم أو معرّف الحاوية ID مثل وسيط لتنفيذ الأمر السابق. وتجدر الإشارة أنه يكفي استخدام بداية المعرّف id عند تمريره وسيطًا، إذ سيعرف دوكر مباشرةً الحاوية المقصودة. $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 48096ca3ffec express-server "docker-entrypoint.s…" 9 seconds ago Up 6 seconds 0.0.0.0:3123->3000/tcp, :::3123->3000/tcp infallible_booth $ docker kill 48 48 لنعمل من الآن وصاعدًا على نفس المنفذ في كلا الجانبين p-. وهكذا لن تضطر إلى تذكُّر ما المنفذ الذي عليك اختياره. إصلاح المشاكل المحتملة الناتجة عن عملية النسخ واللصق لا بُد من تغيير بعض الخطوات لإنشاء ملف Dockerfile متقدم، وقد لا يعمل المثال الذي أوردناه سابقًا على الإطلاق، لأننا أهملنا خطوة هامة. عندما تنفِّذ الأمر npm install على حاسوبك، فقد يُثبّت مدير حزم Node بعض الاعتمادات التي تتعلق بنظام التشغيل أثناء تقدم التثبيت. وقد ننقل صدفةً أجزاءً غير وظيفية إلى الصورة عند استخدام التعليمة COPY، ويحدث ذلك بسهولة إن نسخنا المجلد "node_modules" إلى الصورة. من المهم جدًا إبقاء تلك النقاط في ذاكرتنا عند بناء الصورة، فمن الأفضل أن ننفذ معظم الأعمال مثل npm install أثناء عملية البناء ضمن الحاوية بدلًا من تنفيذها قبل البناء؛ إذ أن القاعدة الجوهرية هنا هي نسخ الملفات التي ستدفعها إلى غيت هب GitHub فقط، ولا ينبغي نسخ الاعتماديات ومتطلبات البناء كونها أشياء يمكن تثبيتها أثناء بناء الصورة. يمكنك استخدام الملف "dockerignore." لحل المشكلة، وهو ملفٌ شبيه بملف التجاهل "gitignore." لمنع نسخ الملفات غير المطلوبة إلى الصورة. يُوضع هذا الملف إلى جوار الملف Dockerfile، وإليك مثالًا عن محتوياته: .dockerignore .gitignore node_modules Dockerfile لكننا سنحتاج إضافةً إلى ملف "dockerignore." في حالتنا إلى تثبيت الاعتماديات خلال خطوة البناء، لهذا سيتغير ملف Dockerfile إلى الشكل: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm install CMD DEBUG=playground:* npm start قد يكون تنفيذ الأمر npm install خطرًا، لهذا يزوّدنا npm بأداة أفضل لتثبيت الاعتماديات وهو الأمر ci. تُلخّص الاختلافات بين ci و install على النحو التالي : قد يُحدّث install الملف "package-lock.json". قد يُثبِّت install نسخةً مختلفةً من الاعتمادية إن ظهرت المحارف "^" أو "~" في نسخة الاعتمادية. سيحذف ci المجلد "node_modules" قبل تثبيت أي شيء. سيتبع ci الملف "package-lock.json" ولا يبدّل أي ملف. باختصار: يقدم ci نسخًا يمكن الاعتماد عليها، بينما يُستخدم install عند تثبيت اعتماديات جديدة. طالما أننا لن نثبِّت أي شيء جديد في خطوة البناء، ولا نريد تغييرات فجائية في النسخ، سنستخدم الأمر ci: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci CMD DEBUG=playground:* npm start كما يمكننا تحسين الحالة أكثر باستخدام الأمر npm ci --only=production كي لا نهدر الوقت في تثبيت الاعتماديات. سيحذف ci المجلد "node_modules" كما أشرنا قبل قليل، وبالتالي لن نضطر إلى إنشاء الملف "dockerignore.". مع ذلك، يُعد هذا الملف أداةً رائعةً عندما تريد تحسين عملية البناء، وسنتحدث باختصار عن هذا الموضوع لاحقًا. ينبغي أن يعمل الملف من جديد، لهذا حاول تنفيذ الأمر التالي: docker build -t express-server . && docker run -p 3000:3000 express-server لاحظ كيف وصلنا هنا أمري باش bash باستخدام &&، وسنحصل تقريبًا على نفس النتيجة إذا نفذنا كلا الأمرين كلًّا على حدة؛ لكن عندما تربط أمرين باستخدام &&، فلن يُنفَّذ الأمر الآخر إذا فشل تنفيذ أحدهما. ضبطنا سابقًا متغير البيئة :DEBUG=playground من خلال الأمر CMD لتشغيل npm، كما يمكننا أيضًا استخدام التعليمة ENV في ملف Dockerfiles لضبط متغيرات البيئة، فلنفعل ذلك: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci ENV DEBUG=playground:* CMD npm start أفضل الممارسات المتعلقة باستخدام Dockerfiles عليك اتباع القاعدتين الجوهريّتين التاليتين عند إنشاء الصور: حاول أن تبني صورةً آمنة قدر المستطاع. حاول أن تُنشئ صورةً صغيرة قدر الإمكان. تُعد الصور الأصغر أكثر أمانًا لأن مجال الهجوم عليها محدود، كما يمكن نقلها بسرعة أكبر ضمن أنابيب النشر. وأخيرًا لا بُد من إصلاح آخر نقطة أهملناها وهي تشغيل التطبيق مثل جذر بدلًا من مستخدم بصلاحيات منخفضة: FROM node:16 WORKDIR /usr/src/app COPY --chown=node:node . . RUN npm ci ENV DEBUG=playground:* USER node CMD npm start التمرين 12.5: إنشاء حاوية لتطبيق Node يضم المستودع الذي نسخته في التمرين الأول تطبيق لائحة مهام todo-app. اطلع على الواجهة الخلفية "todo-app/todo-backend" للتطبيق واقرأ الملف "اقرأني README". لن نقترب حاليًا من الواجهة الأمامية "todo-frontend" الخطوة الأولى: وضع الواجهة الخلفية "todo-backend" ضمن حاوية بإنشاء الملف "todo-app/todo-backend/Dockerfile" ثم بناء الصورة. الخطوة الثانية: تشغيل الصورة على المنفذ الصحيح، والتأكد أن عدّاد الزيارات سيزداد عند استخدامه عبر المتصفح على العنوان "/http://localhost:3000" (أو على أي منفذ آخر قد تهيّئه). تلميح: شغّل التطبيق خارج الحاوية أولًا للتحقق منه قبل وضعه في الحاوية. استخدام الأداة docker-compose أنشأنا في جزئية سابقة من هذه السلسلة خادمًا وعلمنا أنه يعمل على المنفذ 3000 وشغّلناه باستخدام الأمر: docker build -t express-server . && docker run -p 3000:3000 express-server ويبدو أننا سنحتاج إلى سكربت لتذكر هذه التعليمات، لكن لحسن الحظ يقدّم دوكر لنا حلًا أفضل. تُعد الأداة Docker-compose من الأدوات الرائعة الأخرى التي تساعدك على إدارة الحاوية، لهذا سنبدأ استخدام هذه الأداة خلال رحلتنا في دراسة الحاويات، إذ ستساعد على توفير بعض الوقت عند تهيئة الحاوية. ثبّت الأداة Docker-compose ثم تأكد من عملها على النحو التالي: $ docker-compose -v docker-compose version 1.29.2, build 5becea4c سنحوّل الآن الأوامر السابقة إلى ملف yaml يمكن تخزينه في مستودع غيت Git. أنشئ الملف "docker-compose.yml" وضعه في جذر المشروع إلى جوار ملف Dockerfile، ثم ضع المحتوى التالي ضمنه: version: '3.8' # نسخة جديدة لا بد أن تعمل services: app: # اسم الخدمة وقد يكون أي شيء image: express-server # صرّح عن الصورة التي تريد استخدامها build: . # حدد مكان بناء الصورة إن لم تكن موجودة ports: # حدد المنفذ الذي يرتبط به التطبيق - 3000:3000 وضعنا شرحًا لكل سطر إلى جواره، لكن إذا أردت معرفة المواصفات الكاملة فعُد إلى التوثيق. سنتمكن الآن من استخدام الأمر docker-compose up في بناء وتشغيل التطبيق، وإن أردت إعادة بناء الصور، استخدم الأمر docker-compose up --build. بإمكانك أيضًا تشغيل التطبيق في الخلفية باستخدام الأمر docker-compose up -d (الراية d- لفصل التطبيق) وإيقافه بتنفيذ الأمر docker-compose down. يُصرِّح إنشاء الملفات بهذه الطريقة عمّا تريده بدلًا من ملفات السكربت التي عليك تنفيذها وفق ترتيبٍ محدد أو عددٍ محددٍ من المرات، وهذه ممارسةٌ جيدةٌ جدًا. التمرين 12.6: الأداة docker-compose أنشئ الملف "todo-app/todo-backend/docker-compose.yml" الذي يعمل مع تطبيق node من التمرين السابق. وعليك الانتباه إلى عدّاد الزيارات فهو الميزة الوحيدة التي ينبغي أن تعمل. استخدام الحاويات في مرحلة التطوير يمكن استخدام الحاويات أثناء تطوير التطبيقات بطرق متعددة لتسهيل عملك، ومن إحدى فوائدها تجاوز تثبيت وتهيئة الأدوات مرتين. قد لا يكون خيارك الأفضل أن تنقل كامل بيئة التطوير إلى الحاوية، لكن إذا أردت ذلك فهذا ممكن. سنعود إلى هذه الفكرة في آخر القسم، لكن حتى ذلك الوقت عليك تشغيل تطبيق node بنفسه خارج الحاويات. يستخدم التطبيق الذي تعرفنا عليه في التمارين السابقة MongoDB، لهذا دعونا نستخدم Docker Hub لإيجاد صورة MongoDB، إذ أنه المكان الافتراضي الذي نسحب الصور منه، كما يمكنك استخدام مسجلات أخرى أيضًا، لكن طالما أننا نتعمق في دوكر فهو خيارٌ جيد. يمكنك أن تجد من خلال بحث سريع الصورة المطلوبة على العنوان https://hub.docker.com/_/mongo. أنشئ ملف yaml يُدعى "todo-app/todo-backend/docker-compose.dev.yml" يحتوي ما يلي: version: '3.8' services: mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database يُوضَّح معنى أول متغيري بيئة معرفين في الشيفرة السابقة في صفحة Docker Hub: "تُنشِئ المتغيرات المُستخدمة على التوازي مستخدمًا جديدًا وتضبط كلمة المرور له. يُنشأ هذا المستخدم في قاعدة بيانات إدارة الاستيثاق ويعطى دور الجذر root، وهو دور المستخدم الأعلى superuser." يخبر متغير البيئة الأخير MONGO_INITDB_DATABASE قاعدة البيانات MongoDB أن تنشئ قاعدة بيانات بهذا الاسم. بإمكانك استخدام الراية f- لتخصيص ملف لتشغيل أمر Docker Compose مثل: docker-compose -f docker-compose.dev.yml up شغًل الآن MongoDB من خلال الأمر: docker-compose -f docker-compose.dev.yml up -d أما الراية d- فلتشغيل العملية في الخلفية. يمكنك متابعة سجلات الخرج بتنفيذ الأمر: docker-compose -f docker-compose.dev.yml logs -f وتُستخدم الراية f- للتأكد من متابعة السجلات. لا نحتاج حاليًا لتشغيل تطبيق Node ضمن الحاوية، فهذا أمرٌ ينطوي على قدر من التحدي، لكننا سنكتشف هذا الخيار في آخر قسم. نفِّذ الأمر القديم npm install على جهازك لإعداد تطبيق Node، ثم شغل التطبيق باستخدام متغيرات البيئة اللازمة. يمكنك تعديل الشيفرة لجعل متغيرات البيئة متغيرات افتراضية أو استخدم الملف env.. لا ضرر من وضع هذه المفاتيح على غيت هب لأنها تُستخدم ضمن بيئة التطوير المحلية، لذلك سأضعها هناك عن طريق الأمر npm run dev لمساعدتك في النسخ واللصق. $ MONGO_URL=mongodb://localhost:3456/the_database npm run dev لن يكون ذلك كافيًا، بل نحتاج إلى إنشاء مستخدم نستوثِق منه ضمن الحاوية، إذ سيقود الولوج إلى العنوان " http://localhost:3000/todos" إلى خطأ في الاستيثاق. [nodemon] 2.0.12 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): *.* [nodemon] watching extensions: js,mjs,json [nodemon] starting `node ./bin/www` (node:37616) UnhandledPromiseRejectionWarning: MongoError: command find requires authentication at MessageStream.messageHandler (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/mongodb/lib/cmap/connection.js:272:20) at MessageStream.emit (events.js:314:20) ربط وتركيب وتهيئة قاعدة البيانات ستجد في صفحة MongoDB Docker Hub تحت عنوان " تهيئة نسخة جديدة Initializing a fresh instance" معلومات عن تنفيذ شيفرة لتهيئة قاعدة البيانات ومستخدم لها. في المشروع التجريبي ملف "todo-app/todo-backend/mongo/mongo-init.js" يضم المحتوى التالي: db.createUser({ user: 'the_username', pwd: 'the_password', roles: [ { role: 'dbOwner', db: 'the_database', }, ], }); db.createCollection('todos'); db.todos.insert({ text: 'Write code', done: true }); db.todos.insert({ text: 'Learn about containers', done: false }); يهيئ الملف قاعدة البيانات مع مستخدم وبعض المهام المخزّنة في القاعدة، وعلينا في الخطوة التالية نقلها إلى الحاوية وتشغيلها. من الممكن إنشاء صورة جديدة من mongo ثم نسخ الملف إلى الداخل، أو استخدام الأمر لتركيب الملف ضمن الحاوية، لكننا سنختار الطريقة الأخيرة. إن التركيب بالربط Bind mount هي عملية ربط ملف على حاسوبك المحلي بملف في الحاوية، ويمكننا تنفيذ ذلك باستخدام الراية v- مع الأمر بالصيغة التالية: v FILE-IN-HOST:FILE-IN-CONTAINER-. يُعرّف التركيب بالربط تحت المفتاح volumes في الملف "docker-compose"، أما صياغة التصريح فتكون على النحو التالي "مضيف ثم حاوية": mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js إن النتيجة هي أنّ الملف "mongo-init.js" في مجلد mongo على حاسوبك سيكون نفسه الملف "mongo-init.js" في المجلد "docker-entrypoint-initdb.d/" من الحاوية، وسيؤدي تعديل أحدهما إلى تعديل الآخر. لا حاجة لتغيير أي شيء أثناء التشغيل، وهذا هو مفتاح تطوير البرمجيات ضمن الحاويات. نفّذ الأمر التالي للتأكد من وجود كل شيء في مكانه: docker-compose -f docker-compose.dev.yml down --volumes ثم ابدأ لائحة جديدة بتنفيذ الأمر التالي لتهيئة قاعدة البيانات: docker-compose -f docker-compose.dev.yml up إذا واجهك خطأ على النحو التالي: mongo_database | failed to load: /docker-entrypoint-initdb.d/mongo-init.js mongo_database | exiting with code -3 فقد يكون لديك مشكلةً في إذن القراءة، وهذا أمرٌ قد يحدث عند التعامل مع الأقراص volumes. بإمكانك في حالتنا استخدام الأمر chmod a+r mongo-init.js الذي يمنح أيًا كان إمكانية قراءة الملف، لكن كن حذرًا عند استخدام التعليمة chmod لأن السماح بإذونات أكبر قد يقود إلى مشاكل أمنية، لهذا استخدم تلك التعليمة على الملف "mongo-init.js" الموجود على جهازك فقط. سيعمل تطبيق express الآن عبر متغيرات البيئة الصحيحة: $ MONGO_URL=mongodb://the_username:the_password@localhost:3456/the_database npm run dev لنتأكد أن الطلب إلى العنوان http://localhost:3000/todos سيعيد كل المهام الموجودة في قاعدة البيانات، فمن المفترض أن يعيد المهمتان اللتان هيأناهما، ولا بد من استخدام Postman لاختبار وظائف التطبيق الأساسية مثل إضافة وحذف مهام من قاعدة البيانات. البيانات المقيمة في الأقراص لا تُخزّن الحاويات بياناتنا افتراضيًا، فعندما تغلق حاوية قد تستطيع أو لا تستطيع استعادة البيانات. وبصورةٍ عامة هناك طريقتان مختلفتان لتخزين البيانات: التصريح عن مكان ضمن منظومة الملفات (عملية الربط بالتركيب bind mount). ترك الأمر لبرنامج دوكر كي يقرر تخزين البيانات (استخدام الأقراص volume) يُفضّل الخيار الأول عادةً في معظم الحالات التي تحتاج فيها حقًا إلى تفادي حذف البيانات، وسنرى الأسلوبين بالتطبيق العملي: services: mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js - ./mongo_data:/data/db ستُنشئ الإعدادات السابقة مجلدًا يُدعى mongo_data ضمن منظومة الملفات في حاسوبك ثم تربطه بالحاوية بالاسم data/db/. أي أنّ البيانات الموجودة في المجلد data/db/ ستُخزّن خارج الحاوية لكن بإمكانها الوصول إليها. وتذكر إضافة المجلد إلى ملف التجاهل "gitignore.". يمكن تحقيق الأمر ذاته باستخدام أقراص التخزين المسماة: services: mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js - mongo_data:/data/db volumes: mongo_data: يُنشأ القرص الآن ويدار من قبل دوكر، وبإمكانك قبل تشغيل التطبيق استعراض الأقراص الموجودة بتنفيذ الأمر docker volume ls، أو فحصّها docker volume inspect، أو حذفها docker volume rm. ليس اختيار مكان تخزين البيانات محليًا في هذا الخيار أمرًا قليل الأهمية موازنةً بالخيار السابق. التمرين 12.7: كتابة القليل من الشيفرة للتعامل مع MongoDB نفترض في هذا التمرين أنك أنجزت جميع الإعدادات التي تحدثنا عنها سابقًا بعد التمرين 12.5. وسيبقى تشغيل التطبيق خارج الحاوية وستوضع قاعدة البيانات MongoDB فقط ضمن الحاوية. لا توجد طريق ملائمة حتى الآن للحصول على مهمة واحدة (GET* /todos/:id*) وتحديث مهمة واحدة (PUT* /todos/:id*). جد حلًا لهاتين المشكلتين. تنقيح المشاكل في الحاويات لا بُد من تعلم استعمال بعض الأدوات لتنقيح التطبيقات ضمن الحاويات، لأننا لا نستطيع استخدام الأمر console.log دائمًا. عندما تظهر الثغرات في شيفرتك، فلا بد وأن يعمل شيء ما في شيفرتك لتنطلق منه. وعمومًا هناك وضعان لتبدأ منهما: الأول هو تطبيق يعمل والثاني هو تطبيق لا يعمل، لهذا سنطلع على بعض الأدوات التي تساعد في تنقيح التطبيق في الحالة الثانية. يمكن أن تنتقل أثناء تطوير البرنامج في عملك خطوة خطوة لتتاكد طوال الوقت أن كل شيء يعمل كما تتوقع. لكن لا ينطبق هذا الأمر عند ضبط الإعدادات، فقد تخفق الإعدادات التي تكتبها حتى لحظة الإنتهاء منها؛ لهذا إذا كتبت ملف "docker-compose.yml" طويل أو ملف Dockerfile ولم يعمل، عليك التروي برهة والتفكير بالطرق المختلفة التي تتأكد منها أن شيء ما يعمل بالشكل المطلوب. لا تزال طريقة التشكيك بكل شيء واردة هنا، وكما قلنا في القسم الثالث: عليك أن تكون منظّما، وطالما أنّ المشكلة قد تكون في أي مكان، عليك التشكيك بكل شيء، وإزالة كل مصادر الخطأ واحدًا تلو الآخر. فالتوقف والتفكير بالمشكلة بدلًا من كتابة مزيدٍ من الإعدادات هي الطريقة الأنسب في الحصول على حل بسيط، كما أن البحث السريع باستخدام محركات البحث قد يساعدك في التقدم. الأمر exec يمكن استخدام الأمر exec للقفز مباشرةً إلى الحاوية وهي تعمل. لهذا سنهيّئ خادم ويب في الخلفية ونجري بعض الإعدادات كي نشغّله ليعرض الرسالة "!Hello, exec" ضمن المتصفح. سنستخدم الخادم Nginx القادر على تخديم الملفات الساكنة، ويدعم الملف "index.html" الذي يمثل الصفحة الافتراضية ويسمح لنا بتعديلها أو استبدالها: $ docker container run -d nginx الأسئلة المطروحة الآن هي: أين سنتوجه عبر المتصفح؟ هل يعمل الخادم بالفعل؟ لكن نعرف كيف نجيب على هذه الأسئلة وذلك باستعراض الحاويات التي تعمل: $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3f831a57b7cc nginx "/docker-entrypoint.…" About a minute ago Up About a minute 80/tcp keen_darwin لقد حصلنا على جواب السؤال الأول فعلًا، فيبدو أن الخادم ينصت إلى المنفذ 80. لهذا سنطفئ الخادم ثم نعيد تشغيله مستخدمين الراية p- للسماح للمتصفح بالوصول إليه: $ docker container stop keen_darwin $ docker container rm keen_darwin $ docker container run -d -p 8080:80 nginx لنلقي نظرةً على التطبيق بالانتقال إلى العنوان http://localhost:8080، وستجد أنّ التطبيق يعرض رسالة خاطئة، لهذا سنقفز إلى الحاوية مباشرةً لإصلاحها. أبقِ متصفحك مفتوحًا، فلا نريد إغلاق الحاوية عند إصلاحها، بل سنستخدم أوامر الطرفية ضمن الحاوية، ولا تنسى استخدام الراية it- لضمان قدرتك على التفاعل مع الحاوية: $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7edcb36aff08 nginx "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:8080->80/tcp, :::8080->80/tcp wonderful_ramanujan $ docker exec -it wonderful_ramanujan bash root@7edcb36aff08:/# بعد أن قفزنا داخل الحاوية، لا بد من إيجاد الملف الخاطئ واستبداله، إذ يتضح لنا من خلال بحث سريع باستخدام محرك بحث أن الملف هو "usr/share/nginx/html/index.html/". لننتقل إلى المجلد ونحذف الملف: root@7edcb36aff08:/# cd /usr/share/nginx/html/ root@7edcb36aff08:/# rm index.html لو انتقلنا الآن إلى العنوان http://localhost:8080 فسنحصل على الصفحة 404 لأننا حذفنا الملف الصحيح، لهذا سنستبدله بملف آخر يضم المحتوى الصحيح: root@7edcb36aff08:/# echo "Hello, exec!" > index.html أعد تحميل الصفحة وسترى كيف يعرض المتصفح الرسالة الصحيحة. وهكذا نرى كيف نستفيد من الأمر exec في التفاعل مع الحاوية. ستُفقد كل التغييرات التي أجريتها عند حذف الحاوية، ولتحفظ هذه التغييرات لا بد من دفعها باستخدام الأمر commit. التمرين 12.8: واجهة سطر أوامر Mongo استخدم script لتسجيل ما تفعله، ثم احفظ الملف بالاسم "script-answers/exercise12_8.txt". حاول الولوج إلى قاعدة البيانات في التمرين السابق أثناء تشغيل MongoDB باستخدام سطر أوامر CLI. يمكنك تنفيذ الأمر باستخدام exec، ثم أضف بعد ذلك مهمة جديدة عبر سطر الأوامر CLI. شغّل سطر الأوامر وأنت ضمن الحاوية باستخدام الأمر mongo. يتطلب سطر أوامر mongo رايتي اسم مستخدم وكلمة مرور للاستيثاق على نحوٍ صحيح،. وستعمل الرايات u root -p example- جيدًا، وتكون القيم مأخوذةً من الملف docker-compose.dev.yml. الخطوة الأولى: شغّل MongoDB. الخطوة الثانية: استخدم الأمر exec للدخول إلى الحاوية. الخطوة الثالثة: افتح واجهة سطر أوامر mongo. عندما تصل إلى واجهة سطر أوامر mongo يمكنك أن تطلب منها عرض قواعد البيانات: > show dbs admin 0.000GB config 0.000GB local 0.000GB the_database 0.000GB للولوج إلى قاعدة البيانات الصحيحة: > use the_database ولإيجاد المجموعات: > show collections todos يمكنك الآن الوصول إلى البيانات الموجودة ضمن تلك المجموعات: > db.todos.find({}) { "_id" : ObjectId("611e54b688ddbb7e84d3c46b"), "text" : "Write code", "done" : true } { "_id" : ObjectId("611e54b688ddbb7e84d3c46c"), "text" : "Learn about containers", "done" : false } أضف مهمةً جديدة نصها: "Increase the number of tools in my toolbelt" مع ضبط حالتها على false. راجع التوثيق لتتعلم إضافة المدخلات إلى قاعدة البيانات، وتأكد من رؤية المهمة الجديدة في تطبيق Express وعند الاستعلام عنه عبر سطر أوامر Mongo CLI. قاعدة البيانات Redis Redis هي قاعدة بيانات من الشكل (مفتاح-قيمة) مما يجعلها قاعدة لحفظ البيانات بهيكلية أدنى من MongoDB مثلًا، فلن تجد فيها مجموعات أو جداول بل كميات من البيانات يمكن الحصول عليها وفقًا لقيم المفاتيح المرتبط بالبيانات (القيم). تعمل قاعدة البيانات Redis ضمن الذاكرة المؤقتة أي أنها لا تحتفظ بالبيانات دائمًا، ومن أفضل طرق الاستفادة منها هي استخدامها مثل مخزن مؤقت لتطبيق cache، إذ تُستخدم المخازن المؤقتة غالبًا لتخزين البيانات التي قد تكون بطيئة عند إحضارها وحفظها حتى تفقد صلاحيتها فيتوجب عليك إحضارها مجددًا بعد ذلك وتخزينها وهكذا. لا علاقة لقاعدة البيانات Redis بالحاويات لكن وطالما أننا قادرين على إضافة أي خدمة مصدرها طرف آخر إلى التطبيق، فلماذا لا نتعلم شيئًا جديدًا؟ التمارين 12.9 - 12.11 التمرين 12.9: إعداد Redis للمشروع هُيّئ خادم Express مسبقًا للتعامل مع Redis ويفتقد فقط إلى متغير البيئة REDIS_URL، إذ يستخدم التطبيق متغير البيئة للاتصال بقاعدة البيانات. اطلع على عمل Redis مع Docker Hub ثم أضفها إلى الملف "todo-app/todo-backend/docker-compose.dev.yml" من خلال تعريف خدمة جديدة بعد mogo. services: mongo: ... redis: ??? بما أن صفحة Docker Hub لا تضم جميع المعلومات الكافية، استخدم غوغل مثلًا في البحث. ستجد المنفذ الافتراضي للقاعدة بإجراء البحث الموضح في الصورة التالية: لا نعلم بعد إن كان الإعداد سيعمل ما لم نجرّب. لن يستخدم التطبيق Redis من تلقاء نفسه طبعًا، وهذا ما سنراه في التمرين التالي. حالما تهيئ Redis وتشغله، أعد تشغيل الواجهة الخلفية ومرر لها متغير البيئة ‍‍REDIS_URL بالصيغة redis://host:port. $ REDIS_URL=insert-redis-url-here MONGO_URL=mongodb://localhost:3456/the_database npm run dev يمكنك الآن اختبار الإعداد بإضافة السطر التالي: const redis = require('../redis') إلى مثال خادم Express في الملف "routes/index.js". إن لم يحدث شيء فقد طُبِّق الإعداد بصورةٍ صحيحة وإلا سينهار الخادم: events.js:291 throw er; // Unhandled 'error' event ^ Error: Redis connection to localhost:637 failed - connect ECONNREFUSED 127.0.0.1:6379 at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16) Emitted 'error' event on RedisClient instance at: at RedisClient.on_error (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/redis/index.js:342:14) at Socket.<anonymous> (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/redis/index.js:223:14) at Socket.emit (events.js:314:20) at emitErrorNT (internal/streams/destroy.js:100:8) at emitErrorCloseNT (internal/streams/destroy.js:68:3) at processTicksAndRejections (internal/process/task_queues.js:80:21) { errno: -61, code: 'ECONNREFUSED', syscall: 'connect', address: '127.0.0.1', port: 6379 } [nodemon] app crashed - waiting for file changes before starting... التمرين 12.10 ثُبتت قاعدة البيانات https://www.npmjs.com/package/redis في المشروع مسبقًا وأضيفت إليه دالتين تعيدان وعودًا وهما getAsync و setAsync: تقبل الدالة setAsync قيمةً ومفتاحًا، ويُستخدم المفتاح لتخزين القيمة. تقبل الدالة getAsync مفتاحًا وتعيد قيمته المقابلة في الوعد promise. أنجز عدادًا للمهام يخزّن عدد المهام التي تُنشئها في قاعدة البيانات Redis: الخطوة الأولى: زد العداد بمقدار واحد في أي لحظة يُرسل فيها طلب لإضافة مهمة. الخطوة الثانية: أنشئ وصلة GET/statics ساكنة يمكنك طلب معلومات وصفية منها، وينبغي أن تُعاد بصيغة JSON على النحو التالي: { "added_todos": 0 } التمرين 12.11 استخدم script لتسجيل ما تفعله، ثم تحفظ الملف بالاسم "script-answers/exercise12_11.txt". إن لم يسلك التطبيق السلوك المطلوب، فقد يساعدك الولوج المباشر إلى قاعدة البيانات في الدلالة على المشاكل. لنلقي نظرةً إذًا على طريقة استخدام واجهة سطر أوامر Redis في الوصول إلى قاعدة البيانات: انتقل إلى حاوية redis باستخدام الأمر docker exec، ثم افتح الواجهة redis-cli. ابحث عن المفتاح الذي استخدمته باستخدام الأمر * KEYS. تحقق من قيمة المفتاح من خلال الأمر GET. راجع أوامر redis-cli للبحث عن الأمر المناسب لضبط قيمة العداد على 9001. تأكد من أن القيمة الجديدة ستعمل عند تحديث الصفحة http://localhost:3000/statistics. احذف المفتاح باستخدام واجهة سطر الأوامر وتأكد أن العداد سيعمل عند إضافة مهام جديدة. الذاكرة المقيمة وقاعدة البيانات Redis أشرنا سابقًا أنّ قاعدة البيانات Redis لا تحتفظ بالبيانات افتراضيًا، لكنه أمر يمكن حله بسهولة، إذ كل ما علينا فعله هو تشغيل Redis بأمر مختلف كما توضح صفحة Docker hub: services: redis: # أي شيء آخر command: ['redis-server', '--appendonly', 'yes'] # CMD تعديل volumes: # التصريح عن القرص - ./redis_data:/data ستقيم البيانات الآن في المجلد على الجهاز المُضيف، وتذكّر إضافة المجلد إلى الملف gitignore.. إمكانات أخرى لقاعدة البيانات Redis تقدم Redis عدّة ميزات إضافةً إلى عمليات إضافة وضبط وحذف المفاتيح، فهي تحدد مثلًا فترة صلاحية المفتاح وهذه ميزة شديدة الأهمية عند استعمالها مثل مخزن مؤقت. كما يمكن استخدام Redis في إنجاز نماذج نشر-اشتراك publish-subscribe -أواختصارًا PubSub-، وهي آلية تواصل غير متزامنة للتطبيقات الموزّعة. تعمل Redis في هذه الحالة مثل وسيط بين تطبيقين أو أكثر، ينشر بعضها رسائل بإرسالها إلى Redis وعند وصول هذه الرسائل تُبلغ Redis جميع الأطراف بأنها اشتركت بهذه الرسائل. التمرين 12.12 البيانات المقيمة و Redis تأكد أن البيانات لا تُخزّن افتراضيًا في قاعدة البيانات Redis بأن تكون قيمة العداد 0 بعد تنفيذ الأمرين docker-compose -f docker-compose.dev.yml down و docker-compose -f docker-compose.dev.yml up. أنشئ بعد ذلك قرصًا للبيانات عن طريق تعديل الملف "todo-app/todo-backend/docker-compose.dev.yml"، وتأكد من بقاء البيانات بعد تنفيذ الأمرين docker-compose -f docker-compose.dev.yml down وdocker-compose -f docker-compose.dev.yml up. ترجمة -وبتصرف- للفصل Building and Configuring Environments من سلسلة Deep Dive Into Modern Web Development اقرأ أيضًا المقال السابق: مدخل إلى الحاويات أبرز المفاهيم التي يجب عليك الإلمام بها عن الحاويات حاوية دوكر Docker ومخزن APCu في PHP
  17. تُعَدّ CSS أو التنسيقات الانسيابية Cascading Style Sheets الشيفرة التي تنسِّق محتوى ويب، إذ سيقودك هذا المقال عبر الأساسيات اللازمة لتبدأ، كما سيجيب على أسئلة مثل كيف أجعل النص باللون الأحمر؟ كيف سأعرض محتوًى معينًا في منطقة معينة من مخطط الصفحة؟ كيف سأضع صورةً لخلفية الصفحة أو كيف نختار لونًا لها؟ تعرف على لغة CSS لا تُعَدّ CSS لغة برمجة وليست لغة توصيف mark up أيضًا، وإنما هي لغة تنسيق صفحات، إذ تُستخدَم لغة التنسيق هذه لتطبيق تنسيقات مختارة على عناصر HTML، فشيفرة التنسيق التالية مثلًا تجعل لون النص في كل الفقرات النصية <p> باللون الأحمر: p { color: red; } لنجرِّب ذلك، لذا أنشئ ملفًا نصيًا جديدًا وضع فيه الأسطر الثلاثة السابقة، ثم احفظه بالاسم style.css في المجلد styles. لا بد من تطبيق قواعد التنسيق السابقة على مستند HTML وإلا فلن يفيدك ملف التنسيق في شيء، فإذا فاتك شيء مما ذكرناه في المقالات السابقة، فعُد وانظر في مقالي التعامل مع الملفات وأساسيات HTML. افتح الملف index.html انسخ الشيفرة التالية: <link href="styles/style.css" rel="stylesheet"> ضع هذه الشيفرة بين وسمَي البداية والنهاية للعنصر head أي بين <head> و</head>. احفظ التغييرات التي أجريتها على الملف index.html ثم حمّله على متصفحك وسترى ما يشبه الصورة التالية: إذا ظهرت الفقرات النصية باللون الأحمر، فقد أنجزت المطلوب. تشريح مجموعة قواعد لغة CSS لنناقش شيفرة CSS الخاصة بالفقرات النصية ذات اللون الأحمر لكي نفهم طريقة عملها: تُدعى الهيكلية السابقة بمجموعة القواعد ruleset ويُشار إليها غالبًا "بالقواعد" فقط؛ أما الأجزاء التي تتكون منها فهي: المُحدِّد Selector: وهو اسم لأحد عناصر HTML في بداية مجموعة القواعد، ويحدد العنصر أو العناصر التي ستُطبَّق عليها تلك القواعد، وهو العنصر <p> في حالتنا. التصريح Declaration: يمثِّل قاعدةً واحدةً مثل ;color: red، ويحدد الخاصية التي تريد تنسيقها من خواص العنصر. الخواص Properties: وهي الطرق التي تستطيع من خلالها تنسيق عناصر HTML، إذ تمثِّل color في مثالنا خاصيةً لتنسيق العنصر <p>، كما يمكنك من خلال CSS اختيار الخاصيات التي تريد أن تستهدفها قاعدة معينة. قيمة الخاصية Property Value: وتكتب إلى يمين الخاصية بعد النقطتين، إذ يمكنك اختيار واحدة من عدة خيارات ممكنة لهذه الخاصية، فهناك مثلًا الكثير من الألوان وليس فقط اللون الأحمر red. انتبه إلى التفاصيل التالية في صياغة القواعد: القوسان المعقوصان ({}? توضع بعد المحدد، وينبغي أن توضع كل مجموعة من القواعد ضمن قوسين معقوصين. النقطتان الرأسيتان (:? ينبغي استخدامها لفصل الخاصية عن قيمتها أو قيمها في كل تصريح. الفاصلة المنقوطة (;? لفصل كل تصريح عن الذي يليه. لتغيير عدة خواص معًا ضمن مجموعة قواعد واحدة، اكتب التصريح اللازم لكل منها وافصل بين التصاريح بفاصلة منقوطة: p { color: red; width: 500px; border: 1px solid black; } استهداف عدة عناصر بمجموعة قواعد واحدة إن أردت استهداف عدة عناصر بمجموعة قواعد واحدة لكي تتجنب التكرار، فاكتب بكل بساطة أسماء عناصر HTML التي تريد تطبيق القواعد عليها بحيث تفصل بينها فواصل , كما يلي: p, li, h1 { color: red; } دورة تطوير واجهات المستخدم ابدأ عملك الحر بتطوير واجهات المواقع والمتاجر الإلكترونية فور انتهائك من الدورة اشترك الآن أنواع مختلفة من المحددات يشرح المثال الذي أوردناه سابقًا ما يُعرف بمحدِّد العنصر element selector وهو النوع الأول من أنواع المحددات الذي يطبق قواعد تنسيق معينة على عنصر أو عناصر HTML محددة الاسم، لكن هناك أنواع أخرى سنستعرض ما شاع منها فيما يلي: مُحدِّد مُعرِّف العنصر ID selector: يستهدف عنصر ذو مُعرِّف محدد، إذ يمتلك كل عنصر من عناصر صفحة HTML مُعرّفًا فريدًا، وإليك مثالًا كما يلي: #my-id { color: red; } يستهدِف هذا المحدِّد العنصر <a> في الشيفرة التالية: <p id="the-id">somthing here</p> <a id="my-id">somthing here</a> مُحدَّد الصنف class selector: يستهدف جميع العناصر التي تمتلك القيمة نفسها للسمة class، كما يمكن لعدة عناصر أن تأخذ قيمةً واحدةً للسمة class، وإليك المثال التالي: .my-class { color: red; } يستهدف هذا المحدد العنصرين <p> و <a>: <h2 class="x">some subtitle here</h2> <p class="my-class">somthing here</p> <a class="my-class">somthing here</a> مُحدِّد السمة attribute selector: يستهدف العنصر أو العناصر التي تمتلك السمة نفسها كما في المثال التالي: ol[type] { color: red; } يستهدف هذا المحدد القوائم المرتبة التي تمتلك الصفة type: <h2 class="x">some subtitle here</h2> <ol type="I"><li>list item 1</li><li>list item 2</li><ol>//هذه فقط <ol><li>list item 1</li><li>list item 2</li><ol> محدد الصنف المجرّد psuedo-class selector: يستهدف عناصر محددة بالاسم عندما تكون في حالة معينة، مثل مرور مؤشر الفأرة Hover فوق رابط، وإليك مثالًا: a:hover { color: red; } عندما يمر مؤشر الفأرة فوق أي رابط سيتحول لونه إلى الأحمر. يمكنك الاطلاع على أنواع أخرى من المحددات في توثيق CSS العربي في موسوعة حسوب. خطوط الكتابة والنصوص بعد اطلاعنا على بعض مبادئ CSS سنحاول تطبيق ما تعلمناه في تحسين مظهر صفحتنا التجريبية index.html وذلك بإضافة بعض القواعد والمعلومات في الملف style.css. أولًا، جد أولًا شيفرة الخط الذي اخترته سابقًا من خطوط جوجل وخزنته. ثانيًا، أضف الشيفرة التالية ضمن عنصر <link> الموجود داخل العنصر <head> في الملف index.html لتبدو كما يلي: <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet"> ستربِط هذه الشيفرة الملف إلى ملف تنسيق تُعرِّف عائلة خطوط الكتابة "Open Sans" مع صفحتك. ثالثًا، احذف القاعدة الموجودة في الملف style.css، فقد استخدِمت لتجربة تنسيق العناصر. رابعًا، أضف أسطر الشيفرة التالية، بعد تبديل font-family بما قد خزّنته سابقًا: html { font-size: 10px; /* px means "pixels": the base font size is now 10 pixels high */ font-family: "Open Sans", sans-serif; /* this should be the rest of the output you got from Google fonts */ } تشير الخاصية font-family إلى اسم الخط الذي تريد استخدامه، بينما تشير الخاصية font-size إلى حجمه، وقد اخترنا محدِّد العنصر html تحديدًا، لكي نعتمد حجم الخط ذاك أساسًا في الصفحة بالكامل، إذ العنصر html هو العنصر الأب لكل العناصر في الصفحة التي تتفرع منه لذا فهي ترث منه حجم الخط ونوعه. ملاحظة: كل ما يرد بين المجموعتين /* و */ هو تعليق في CSS، وسيتجاهلة المتصفح عندما يُصيّر الشيفرة. خامسًا، لنضبط حجم الخط للعناصر التي تقع داخل مستند HTML وهي <h1> و <li> و <p>، ولنضبط موقع العنصر <h1> ليظهر في منتصف الصفحة، وأخيرًا سنضبط ارتفاع سطر الكتابة والتباعد بين الأسطر للعنصرين <li> و <p> من خلال الخاصيتين line-height و letter-spacing على التوالي لتسهيل قراءة المحتوى: h1 { font-size: 60px; text-align: center; } p, li { font-size: 16px; line-height: 2; letter-spacing: 1px; } اضبط قيمة الحجم بواحدة px كما تريد وسيبدو عملك الآن على نحو مشابه للصورة التالية: فكرة الصناديق في لغة CSS قد تلاحظ في مرحلة ما عند كتابة قواعد CSS أنها تعتمد فكرة الصناديق في ضبط الأحجام والألوان ومواقع العناصر، إذ يمكنك التفكير في عناصر HTML على أساس أنها صناديق فوق بعضها. يعتمد تخطيط CSS بأكمله تقريبًا على نموذج الصندوق box model، إذ يشغل كل صندوق حيزًا من مساحة الصفحة ويتمتع بخصائص مثل: padding: الحشو أو الحاشية، وهو الفراغ المحيط بمحتوى العنصر، ويمثل في الشكل الذي يُعرض تاليًا الفراغ المحيط بمحتوى الفقرة النصية. border: الإطار، وهو الخط الصلب الذي يحيط بمنطقة الحشو. margin: الهامش، وهو الفراغ الخاص بالعنصر ويحيط بالإطار. سنستخدِم أيضًا في هذا القسم: width: يضبط عرض العنصر. background-color: يضبط لون الخلفية حول المحتوى ومنطقة الحشو. color: يضبط لون المحتوى الذي يضمه العنصر. text-shadow: ويضبط تدرج الظل حول النص داخل العنصر. display: يضبط نمط عرض العنصر، ويمكنك استئناف القراءة لتفهم أكثر. لنكمل بإضافة بعض قواعد CSS ونضعها في أسفل الملف style.css، ويمكنك تغيير القيم كما تشاء ومراقبة ما يحدث. تغيير لون الصفحة html { background-color: #00539F; } تضبط هذه القاعدة لون خلفية الصفحة بالكامل. غيّر قيمة اللون 00539F# بالطريقة التي تعلمناها في مقالات سابقة وراقب التغيّر الذي يحدث. تنسيق جسم الصفحة في لغة CSS body { width: 600px; margin: 0 auto; background-color: #FF9500; padding: 0 20px 20px 20px; border: 5px solid black; } لنناقش القواعد السابقة التي تنسق العنصر body: width: 600px: تحدِّد قياس جسم الصفحة ليبقى دائمًا 600 بكسل. margin: 0 auto: عندما تضع قيمتين لخاصية مثل margin أو padding، فستضبط القيمة الأولى الحدود العليا والدنيا على 0 في هذه الحالة، بينما ستضبط القيمة الثانية الحدَّين اليساري واليميني على القيمة auto، وهي قيمة خاصة تقسِّم الحيز الأفقي المتوفر بالتساوي بين اليمين واليسار، ويمكنك أيضًا استخدام ثلاث قيم أو أربع للخاصية margin. background-color: #FF9500: تضبط لون خلفية العنصر، إذ نستخدِم في مشروعنا اللون البرتقالي المائل إلى الحمرة وهو اللون المتمِّم للون الأزرق الداكن الذي نستخدِمه للعنصر <html>، كما يمكنك تجريب ما تشاء. padding: 0 20px 20px 20px: تضبط قيم الأطراف الأربعة للحاشية المحيطة بالعنصر، والغاية من الحاشية إضافة مساحة فارغة حول العناصر، إذ تدل القيم المبينة على عدم وجود حاشية أعلى جسم الصفحة ووجود حاشية مقدارها 20 بكسل حول بقية الأطراف، كما يمكنك أيضًا استخدام قيمة واحدة للخاصية padding أو قيمتين أو ثلاث أيضًا. border: 5px solid black: يضبط قيم عرض وتنسيق ولون الإطار المحيط بالعنصر، فهو خط مستمر solid وأسود بعرض 5 بكسل في حالتنا ويحيط بكامل جسم الصفحة. ضبط موقع عنوان الصفحة وتنسيقه h1 { margin: 0; padding: 20px 0; color: #00539F; text-shadow: 3px 3px 1px black; } قد تلاحظ فراغًا مخيفًا أعلى جسم الصفحة، وذلك لأن المتصفح سيطبق التنسيق الافتراضي للعنصر <h1>، وقد تبدو الفكرة سيئةً، لكنه أمر ضروري لتحسين القراءة في الصفحات غير المنسقة، لذا سنتجاوز التنسيق الافتراضي للمتصفح ونضبط الحاشية على القيمة 0 باستخدام التصريح ;margin: 0 لنحل المشكلة، ثم نعيد ضبط الحاشية العليا والسفلى على القيمة 20 بكسل، كما سنضبط بعد ذلك لون العنوان ليكون مماثلًا للون المتمم للخلفية -أي أزرق غامق-، وأخيرًا سنعرض بعض الظلال حول العنوان باستخدام الخاصية text-shadow والتي تأخذ أربع قيم: تضبط القيمة الأولى الانزياح الأفقي للظل عن النص: كم يبعد عنه مارًا من خلاله. تضبط القيمة الثانية الانزياح العمودي للظل عن النص: كم يبعد عنه نحو الأسفل. تضبط القيمة الثالثة نصف قطر الضبابية blur: كلما كانت القيمة أكبر كلما بدا ظل النص ضبابيًا أكثر. تضبط القيمة الرابعة الأساس اللوني للظل. ضبط الصورة في المنتصف img { display: block; margin: 0 auto; } سنجعل الصورة في مركز الصفحة لتبدو أفضل، وقد نستخدِم حيلة الهوامش السابقة margin: 0 auto، لكن هناك بعض الاختلافات التي تتطلب إعدادات إضافية. يُعّدُ العنصر <body> عنصرًا كتليًا block أي أنه يشغُل مساحةً من الصفحة، كما ستحترم بقية العناصر الهوامش التي يأخدها؛ أما الصور، فهي عناصر مضمنة inline ولا بد من إعطائها صفة كتلية حتى تعمل فكرة الهوامش عن طريقة الخاصية ;display: block. ملاحظة: تفترض التعليمات السابقة أنّ عرض الصورة لا يزيد عن عرض جسم الصفحة الذي ضبطناه ليكون 600 بكسل، فإذا كانت الصورة أكبر، فستخرج عن إطار جسم الصفحة لتغطي بقية أجزائها، كما يمكن حل المشكلة بتصغير حجم الصورة باستخدام محرر صور أو تنسيقها بضبط الخاصية width للعنصر <img> على قيم أصغر. ملاحظة: لا تقلق إذا لم تفهم تمامًا التصريح ;display: block أو الفرق بين العنصر الكتلي والمضمن، إذ سيغدو الأمر أكثر وضوحًا مع تقدمك في تعلِّم CSS. خلاصة إذا اتبعت كل الإرشادات التي تحدثنا عنها في المقال، فستبدو الصفحة التجريبية التي تعمل عليها على النحو التالي: إذا لم تجد عملك صحيحًا، فيمكنك دائمًا موازنة ما فعلته مع النسخة الجاهزة على جيت-هاب. ترجمة -وبتصرف- للمقال CSS Basics. اقرأ أيضًا المقال السابق: أساسيات لغة HTML أساسيات استعمال لغة CSS تقنيات كتابة شيفرات CSS احترافية وسهلة الصّيانة توثيق لغة CSS العربي
  18. تعرفنا في المقال السابق على طريقة ضبط اﻹعدادات البسيطة لشبكات الاتصال المختلفة، كما تعلمنا ضبط إعدادات مظهر سطح المكتب والمواضيع المتعلقة به. نتابع في هذا المقال استكشاف بقية اﻹعدادات التي تختص بإدارة التطبيقات المثبتة وإدارة حسابات المستخدمين. إعدادات خاصة بالتطبيقات افتح تطبيق "اﻹعدادات"، وانتقل منه إلى خيار "التطبيقات". عند النقر على هذا الخيار، سيعرض لك الشريط الجانبي لنافذة اﻹعدادات جميع التطبيقات المثبتة على حاسوبك. انقر اﻵن على أي تطبيق، وستظهر لك نافذة مشابهة للنافذ التالية: تتكون النافذة من قسمين رئيسيين: التكامل: وتعرض مجموعة موارد النظام التي يمكن لهذا التطبيق الوصول إليها. نلاحظ أن أغلب التطبيقات يمكنها فقط عرض تنبيهاتها ورسائلها للمستخدم. وإن أردت إلغاء التنبيهات التي يعرضها تطبيق ما، فانقر على الزالقة المجاور لحقل التنبيهات. المناولات المبدئية: يعرض هذا القسم مجموعة الملفات التي يمكن لهذا التطبيق فتحها، باﻹضافة إلى الروابط التي يمكنه الاتصال بها وتنزيل البيانات منها. تعرض الصورة التالية الملفات المبدئية لتطبيق "الملفات": لاحظ أن التطبيق قادر على فتح المجلدات العادية و 19 نوعًا من الأرشيفات (المجلدات المضغوطة). وﻹزالة أي نوع من هذه اﻷنواع لأسباب تتعلق بك (كأن تفضل أن تُفتح من قبل تطبيق آخر)، انقر على الزر" أزل" إلى جوار نوع الملفات أو اﻷرشيفات الذي لا تريد لهذا التطبيق فتحه. ولكي تطلع على تفاصيل أكثر عن التطبيق الذي اخترته، انقر على زر "افتح في البرمجيات" أعلى النافذة ضمن شريط المهام ليفتح بدوره تطبيق "برمجيات" الذي يعرض لمحةً عن التطبيق، ويتيح تشغيل التطبيق أو إزالته كليًا من حاسوبك. للخروج من قائمة التطبيقات، انقر على أيقونة التراجع الموجودة أقصى يسار شريط مهام النافذة. إعدادات الخصوصية ويُقصد بها اﻹعدادات التي تساعد المستخدم في منع تجاوز خصوصيته من خلال الاطلاع على موقعه أو مراقبة نشاطه على شبكات الاتصال أو الوصول إلى معلومات حساسة عنه. يتيح لك تطبيق "اﻹعدادات"، إمكانية ضبط بعض اﻹعدادات التي تسهم في المحافظة على خصوصية ما تقوم به على حاسوبك، وسيعرض لك التطبيق هذه الخيارات بمجرد النقر على "الخصوصية": الموصولية يتيح لك هذا الخيار إمكانية تشغيل تطبيق "التحقق من الموصولية connectivity checking"، الذي يتفقد تلقائيًا إعدادات شبكة الاتصال التي تستخدمها ويعالج المشاكل التي تظهر ليبقى اتصالك مستقرًا دائمًا؛ كما يساعد هذا التطبيق في الحصول على معلومات عن أي جهاز ضمن الشبكة يحاول مراقبة اتصالك. لتفعيل هذا التطبيق، انقر على الزالقة المجاورة، فتتحول إلى اللون البنفسجي. وانقر مجددًا ﻹلغاء تفعيله. خدمات التموضع يسمح لك هذا الخيار بإلغاء أو تفعيل تحديد مكانك الحقيقي، إذ تطلب بعض التطبيقات اﻹذن للوصول إلى موقعك الجغرافي الحقيقي لتقديم خدمات معينة. لتفعيل خدمة تحديد الموضع، انقر على الزالقة في شريط أدوات النافذة، وعندها ستظهر لك قائمة بالتطبيقات التي تطلب إذنًا في الوصول إلى موقعك الجغرافي. تاريخ الملفات وسلة المهملات تضم هذه اﻹعدادات قسمين من الخيارات: تاريخ الملفات عندما تعمل على ملف معين، سيسجل النظام ذلك ويحدد موقع هذا الملف في منظومة الملفات، كما يحدد التطبيق الذي أنشأه والتطبيق الذي تستخدمه في تحرير هذا الملف. يشارك النظام هذه السجلات بين جميع التطبيقات لكي يسهل وصولك إلى آخر الملفات التي عملت عليها، والتي قد ترغب في إعادة فتحها. قد يحمل هذا اﻷمر تهديدًا ممكنًا للخصوصية، ولذلك يتيح لك النظام القدرة على ضبط الفترة الزمنية التي يحتفظ فيها بتلك السجلات. وبافتراض أنك الوحيد الذي يستثمر هذا الحاسوب مستخدمًا الحساب الذي ولجت فيه، فإن الخيار الافتراضي هو "أبدًا". أي أن هذه السجلات ستبقى محفوظة إن لم يطرأ خلل ما ولن يجري مسحها. في المقابل ترك لك النظام عدة خيارات أخرى هي: يوم واحد 7 أيام 30 يومًا كما يعطيك خيار الحذف الفوري للسجلات من خلال النقر على زر "مسح السجلات clear history". سلة المهملات والملفات المؤقتة يضبط هذا القسم الفترة الزمنية التي تحتفظ بها سلة المهملات أو مخازن الملفات المؤقتة بمحتوياتها. قد تضم هذه اﻷماكن بعض المعلومات الحساسة التي يمكن استرجاعها من قِبل جهات غير مخولة بالاطلاع عليها، لذلك يعطيك النظام إمكانية الحذف التلقائي لهذه الملفات. ستلاحظ وجود ثلاثة خيارات: الحذف التلقائي لمحتويات سلة المهملات ِAutomaticaaly delete trash content: بالنقر على هذا الخيار ستفعل عملية الحذف التلقائي لمحتويات سلة المهملات. الحذف التلقائي للملفات المؤقتة Automatically delete temporary files: بالنقر على هذا الخيار تفعل عملية الحذف التلقائي للملفات المؤقتة. فترة تنفيذ الحذف التلقائي Automatically delete period: ويحدد الفترة الزمنية التي تبقى فيها الملفات المؤقتة ومحتويات سلة المهملات موجودة قبل أن تحذف تلقائيًا. ستجد أيضًا أسفل النافذة زرين: اﻷول "أفرغ المهملات" لحذف محتويات سلة المهملات مباشرةً، واﻵخر "delete temporary files"، لحذف الملفات المؤقتة مباشرةً. إقفال شاشة المستخدم تُستخدم هذه الميزة عادةً لمنع أيٍّ كان من استخدام حسابك على الجهاز إن غادرت مكان عملك لبرهة وتركته مفتوحًا. إذ تُقفل شاشة سطح المكتب في هذه الحالة ويُطلب إليك إدخال كلمة المرور من جديد حتى يسمح لك بمتابعة العمل. يتيح لك هذا القسم من اﻹعدادات أن تضبط مايلي: زمن تعتيم الشاشة blank screen delay: وهي الفترة الزمنية التي يختفي بعدها سطح المكتب عند ترك العمل، ويمكنك ضبط هذه الفترة "من دقيقة واحدة" إلى "أبدًا" وتعني أن يبقى سطح المكتب ظاهرًا دائمًا. إيصاد تلقائي للشاشة: يقفل النظام الشاشة تلقائيًا إن تركت العمل على حاسوبك فترةً من الزمن. الفترة الزمنية للقفل التلقائي للشاشة Automatic screen lock delay: إن اخترت أن تُقفل الشاشة تلقائيًا، فسيحدد لك هذا الخيار الفترة الذي ينتظرها النظام قبل اﻹيصاد التقائي لها. وتتراوح الخيارات بين 30 ثانية حتى ساعة؛ كما يتيح خيار اﻹيصاد بمجرد أن تنطفئ الشاشة. قفل الشاشة عند تعليق العمل lock screen on suspend: عند تفعيل هذا الخيار سيقفل النظام شاشة سطح المكتب عندما تعلّق العمل على الجهاز (النقر على خيار "علّق" ضمن خيارات اﻹطفاء في شريط المهام الرئيسي)، وبالتالي ستضطر إلى كتابة كلمة السر عندما تعيد تشغيل الجهاز. أظهر التنبيهات على شاشة القفل show notifications on lock screen: يمكن لأي تطبيق عند تفعيل هذا الخيار عرض تنبيهاته ورسائله على الشاشة حتى إن كانت موصدة. إدارة المستخدمين انقر على خيار "المستخدمين" الموجود في الشريط الجانبي لنافذة تطبيق "الإعدادات"، وستظهر النافذة التالية: يتطلب اﻷمر عادةً فك الحظر عن تغيير هذه الإعدادات، وذلك بالنقر على الزر "فك unlock" الموجود في الشريط السماوي أعلى النافذة، ثم يطلب إليك النظام عندها إدخال كلمة السر للاستيثاق منك، وستكون بعدها جاهزًا للتحكم بخيارات مثل: تغيير اسم المستخدم: بالنقر على أيقونة القلم بجانب اسم المستخدم. تغيير كلمة السر: يطلب إليك عندها تزويده بالكلمة القديمة ثم الجديدة وتأكيدها. خيار الولوج التلقائي: سينقلك النظام إلى سطح المكتب مباشرةً عند تشغيل الحاسوب دون المرور بشاشة تسجيل الدخول إن فعّلت هذا الخيار. نشاطات الحساب account activity: ويعرض لك قائمةً بوقتي بداية ونهاية كل جلسة على حاسوبك خلال أسبوع. إضافة مستخدم جديد: انقر على زر "أضف مستخدمًا" في شريط مهام النافذة، وسيعرض لك النظام مربع الحوار التالي: اختر نوع الحساب: عادي أو إداري (إن لم ترغب بإعطاء المستخدم صلاحيات واسعة اختر "عادي"). اكتب اسمك. اكتب اسم المستخدم الذي سيكون نفسه اسم مجلد "المنزل" ولا يمكن تغييره لاحقًا. اختر كلمة سر، ثم أكدها. انقر الزر "أضف". اقرأ أيضًا المقال السابق: إعدادات أوبونتو 20.04: الاتصالات وسطح المكتب تعرّف على سطح مكتب أوبونتو 20.04 التعامل مع المجلدات والملفات في أوبونتو 20.04 تغيير اللغة في نظام لينكس أوبنتو إلى العربية
  19. لغات البرمجة هو مفهوم واسع كبير يحتوي على أسماء عدة شتى قد يحتار فيها الداخل الجديد إلى مجال علوم الحاسوب لتعلم البرمجة، فقد يتساءل أي لغة برمجة يجب أن أتعلم أولًا، وما هي أشهر لغة برمجة تُستعمل على نطاق واسع استفيد منها، وما هي أسهل لغة برمجة ابدأ بها وغيرها من الأسئلة المحيرة، فإن فتحت مثلًا قائمة لغات البرمجة على ويكيبيديا فستجد عشرات لغات البرمجة المذكورة وهو ما يزيد المشكلة وهنا لابد من الاستعانة بدليل. يقودك هذا المقال تدريجيًا في جولة سريعة في عالم لغات البرمجة كي تتكون لديك صورة عامة موسّعة عن لغات البرمجة لتكون عونًا لك إن كنت ستتعلم إحداها، وسينتقل بك المقال من شروحات مبسطة لمفهوم لغات البرمجة ومستويات لغة البرمجة وأقسامها إلى أنواعها واستخداماتها سواء لبرمجة الحواسيب أو الأجهزة الذكية أو الأجهزة الإلكترونية وحتى الروبوتات، ثم سنجاوب على بعض الأسئلة الشائعة المتعلقة بلغات البرمجة مثل التي أشرنا إليها قبل قليل في نهاية المقال. ينبغي أن تكوّن في نهاية المقال فكرة واضحة عن ما يُدعى لغة برمجة ولغات البرمجة عمومًا تكون ركيزة صلبة تعتمد عليها في اختيار لغة البرمجة التي تناسب المجال الذي تخطط الدخول له إن أردت تطوير مسارك المهني ودخول عالم البرمجة. إليك فهرس المقال لكي يسهل عليك التنقل بين أقسامه: تعريف البرمجة ما هي لغات البرمجة لغة البرمجة واللغات البشرية أنواع لغات البرمجة لغات برمجة السكربت Scripting Languages اللغات التوصيفية Markup Languages أسئلة شائعة عن لغات البرمجة تعريف البرمجة البرمجة هي العملية التي تستطيع بواسطتها إنجاز فكرة معينة أو حل مشكلة ما عن طريق تقسيم الفكرة أو المشكلة إلى خطوات متتالية قابلة لإعادة التكرار وصولًا إلى النتيجة المطلوبة. فلو سألت أحدهم ما هو ناتج العملية الرياضية التالية 5*(2-3) فقد يعطيك مباشرة الجواب 5 لكن ما فعله الدماغ هو عملية تحليل للمسألة ووضع طريقة لحلها وهذا ما يُدعى بوضع خوارزمية Algorithm. فخوارزمية حل المسألة البسيطة السابقة هي: اطرح 2 من 3 وسجل الجواب: 1 =2-3. احسب جداء الناتج مع العدد 5: 5x1. قل الجواب: 5. وخلاصة القول أن البرمجة هي طريقة لترتيب وتنظيم مسألة ما للحصول على النتيجة بالمعنى العام، أما في عالم الحواسيب فهي وسيلة للتخاطب مع هذه الأجهزة. أما الأسلوب المقترح لتنفيذ هذه الخطوات فهي الخوارزمية، وستكون عندها لغة البرمجة هي الوسيلة التي نخبر فيها الحاسوب أو التجهيزة كيفية تنفيذ الخوارزمية لحل المشكلة المعروضة. ما هي لغات البرمجة؟ لغات البرمجة تشير ببساطة على أنها وسيلة للتواصل بين البشر والحواسب أو بعض التجهيزات أو الآلات المهيأة لتنفيذ برامج متغيرة والتي تُدعى تجهيزات قابلة للبرمجة. ونظرًا للتقدم التكنولوجي الهائل ودخول تقنيات المعلومات إلى مختلف نواحي الحياة، ازداد توجه الصانعين إلى إنتاج تجهيزات قادرة على التخاطب والتفاعل مع المستخدم لتنفيذ وظائف متعددة كالصرافات الآلية ونقاط الخدمة الذاتية والهواتف الذكية وحتى التجهيزات المنزلية والسيارات، وكلما زاد تعقيد هذه التجهيزات وتعددت مهامها احتاجت إلى طريقة فعّالة لتخبرها بما هو مطلوب منها. وهنا تأتي أهمية لغات البرمجة وضرورة وجود أنواع مختلفة من لغات البرمجة وفق سويات مختلفة لتأمين إدارة تلك التجهيزات والتواصل الأمثل معها. إذًا فلغة البرمجة هي مجموعة من التعليمات والتوجيهات التي تكتب أو تُجمّع أو تُركّب ضمن سياق معين كي تنقل بعد معالجتها إلى الجهاز الهدف بغية تنفيذها. ويُقصد بمعالجة لغات البرمجة هو تحويلها من تعليمات مقروءة -في لغات البرمجة المكتوبة Written Programming Languages- أو مرئية بالنسبة للبشر -في لغات البرمجة المرئية Visual programing languages- إلى توجيهات تفهمها الآلة المستهدفة سواء حاسوب أو أية أنظمة إلكترونية أخرى. فما يفهمه الحاسوب هو برنامج مكوّن من الواحدات 1 والأصفار 0 التي تخبره وفقًا لتسلسلها بطريقة محددة سلفًا ما عليه فعله، وتعرف لغة البرمجة التي تُكتب برنامجًا بهذه الطريقة "لغة الآلة" machine language. ونظرًا لصعوبة فهمها للبشر، ظهرت الحاجة إلى لغات برمجة أكثر قربًا من البشر وهنا بدأت الحكاية. تتكون لغة البرمجة -مثلها مثل أي لغة- عمومًا من الأقسام التالية: صياغة لغة البرمجة Syntax هي الطريقة التي نصيغ فيها تعليمات لغة البرمجة ونربطها مع بعضها لإنتاج عبارات صحيحة الصياغة يمكن استخدامها في تنفيذ البرنامج وقد تكون الصياغة: نصية: وتمثل تعليمات اللغة وكلماتها المفتاحية keywords وعباراتها ومتنها. رسومية أو كتلية: تُنظَّم فيها التعليمات التي تؤدي عملًا محددًا ضمن كتلة واحدة، ثم تُمثَّل هذه الكتلة بطريقة مرئية كمربع أو دائرة تُعطى لونا واسمًا يدل على طبيعة العمل الذي تنفذه. ويبنى البرنامج عندها بضم هذه الكتل إلى بعضها لإنجاز الوظيفة المنوطة بالبرنامج. تعطي الصياغة إذًا الشكل العام الصحيح لطريقة كتابة التعليمات بناء على معايير خاصة خارج نطاق مقالنا، وإن أردنا تقريب الأمر فهي بمثابة القواعد النحوية للغات البشر أو اللغات الطبيعية. إذ تُعد مثلًا الجملة "إن المبرمجون مبدعون." في اللغة العربية خاطئة الصياغة لمخالفتها قواعد اللغة ومن المفترض أن نقول" إن المبرمجين مبدعون.". دلالة لغة البرمجة Semantic هي مجموعة قواعد تحدد ما إن كانت طريقة صياغة التعليمات ستعطي النتيجة المرجوة أم لا، وتضع بعض القيود على الصياغة الصحيحة التي قد لا تؤدي إلى نتيجة. لن نخوض كثيرًا في هذه الفكرة لكن سنسهل الأمر عليك عزيزي القارئ: لن تمنعنا أية قاعدة نحوية في اللغة العربية من القول بأنني "أتناول برتقالًا حامضًا لا طعم له" لكن كيف يكون حامضًا ولا طعم له في نفس الوقت! صياغة صحيحة ومدلول لا معنى له. تحديد الأنواع في لغة البرمجة Types وهي الطريقة التي تصنّف فيها لغة البرمجة القيم والتعابير ضمن أنواع مختلفة وكيفية التعامل مع هذه الأنواع والتحويل فيما بينها. فهنالك مثلًا قيم نصية كأن استخدم القيمة "انقر هنا" وقيم عددية كأن استخدم الرقم 5 وقيم منطقية كأن استخدم القيمة "صحيح" true. وقد تكون نتيجة تنفيذ العملية نوعًا محددًا من البيانات كأن تُنتج العملية الحسابية عددًا أو تنتج نصًا. لهذا تحاول معظم اللغات وضع أنواع للقيم التي تتعامل معها. لكن في المقابل ستجد عدة لغات لا تعتمد على الأنواع مثل جافاسكربت JavaScript وماتلاب MatLab. إن وجدت هذا الفكرة غامضة قليلًا لا تكترث وتابع القراءة فستبدو هذه الفكرة غاية الوضوح ما أن تكتب برنامجك الأول في لغة تختارها. المكتبات المعيارية Standard Libraries وهي مجموعة من التعليمات أو العمليات الجاهزة التي توفرها لغة البرمجة لعمل مختلف أجزائها مع بعضها ولتنفيذ المهام الأساسية المنوطة بلغة البرمجة تلك مثل التعامل مع النصوص والأعداد والتواصل مع نظام التشغيل ونظام الملفات وغيرها، وتوضع عادة في ملفات منفصلة وتضاف إلى البرنامج الذي تُنفّذه. إذ تساعدك بعض المكتبات مثلًا على إضافة نصٍ إلى نص آخر مباشرة باستخدام إشارة الجمع + على الرغم من كونها عملية حسابية تجري على الأعداد. إذ تضم تلك المكتبات القدرة على فهم أن هذه العملية هنا ليست لجمع عددين بل لضم نصين. تصنّف لغات البرمجة في فئات ومجموعات وفقًا لصياغتها وطريقة معالجتها للمعلومات وطريقة تنفيذ تعليمات لغة البرمجة وهذا ما سنتوسع فيه بعد قليل. لغة البرمجة واللغات البشرية إنّ اللغات البشرية هي الطريقة الطبيعية للتواصل بين البشر وتتكون من حروف تكوّن كلمات ومن ثم تشكل جملًا وفق مجموعة من القواعد التي ندعوها في العربية "نحوًا" ومن ثم نستخدم هذه الجمل بعناية لإيصال المعنى المطلوب. ويُفترض بالإنسان أن يُلم بأساسيات لغة الشخص الذي يحاوره كي لا تقع مشاكل في التواصل. الأمر مشابه في لغات البرمجة كونها لغة للتخاطب بين البشر والحواسيب أو الآلات التي تقبل البرمجة عمومًا. إضافة إلى ذلك، للغات الطبيعية ولغات البرمجة نوع من الهيكلية أو البنية التي تنتظم وفقها تلك اللغات، فالكلمات في اللغات الطبيعية قد تشابه التعليمات في لغة البرمجة والجمل sentences قد تشابه التعابير البرمجية Expressions، وتستخدم لغات البرمجة ما تستخدمه اللغات الطبيعية من علامات للترقيم لكن لأغراض خاصة بوظيفتها، وتستخدم عوامل تشابه حروف العطف والاختيار والموازنة في اللغات الطبيعية للربط بين التعابير البرمجية واستخلاص المعنى، فلهذه اللغات جميعها أساليب في الصياغة والمدلول. ومن أوجه الشبه أيضًا ما يُدعى بالعائلات، فاللغات الطبيعية تنحدر من عائلات تتشابه اللغات فيها كاللغات السامية أو اللغات الجرمانية أو السلافية. كذلك الأمر في لغات البرمجة التي تنحدر جميعها من لغات ظهرت في البداية مثل Fortran التي انحدرت منها Algol ثم C و ++C إذ تتشابه هاتين الأخيرتين كثيرًا. أما الاختلافات فتأتي من كون التعليمات في لغات البرمجة معدة سلفًا وقابلة للحصر حتى إن تطورت سيكون التطور بإلغاء تعليمة ووضع أخرى بينما تتطور اللغات الطبيعية وفقًا للحاجة ويظهر ذلك تلقائيًا. كذلك يظهر الاختلاف في عدم قدرة لغات البرمجة على التعبير بطرح أسئلة أو استخدام الإيحاءات بل تعتمد على مجموعة قواعد صارمة فيما يخص الصياغة وطريقة استخدام اللغة. تأتي أهمية هذا النقاش من ضرورة الفهم الأصيل للغات البشرية ولغات البرمجة لما سيقود ذلك من تطور في مجال البرمجيات التي تتعرف على الكلام أو المترجمات الآلية أو الذكاء الاصطناعي وغيرها الكثير. أنواع لغات البرمجة هنالك المئات من لغات البرمجة وجدت لتحقيق أهداف مختلفة وحتى هذه اللحظة تظهر لغات برمجة جديدة باستمرار، لذا تصنف هذه اللغات إلى أنواع عدة بالاعتماد على وظائف كل لغة وتطبيقاتها وطريقة معالجتها وغيرها من المقاييس ونوضح في الصورة التالية بعض الأنواع أو الفئات التي تنضوي تحتها لغات البرمجة. أنواع لغات البرمجة الشائعة هي تنقسم إلى الأنواع التالية: أنواع لغات البرمجة وفق مستوى الترميز لغات البرمجة منخفضة المستوى Low level Programming Languages لغة الآلة Machine language لغات التجميع Assembly languages لغات البرمجة عالية المستوى High level languages أنواع لغات البرمجة وفق طريقة معالجة التعليمات اللغات المُصرَّفة Compiled Languages اللغات المُفسَّرة Interpreted languages اللغات الهجينة المصرّفة المفسّرة أنواع لغات البرمجة وفق أسلوب تنظيم الشيفرة لغات البرمجة الوظيفية Functional Programming لغات البرمجة الإجرائية Procedural Programming لغات البرمجة الكائنية Object-oriented Programming أنواع لغات البرمجة وفق مجالات الاستخدام لغات البرمجة عامة الغرض General Purpose programming Languages لغات البرمجة خاصة الغرض Special Programming Languages شرح كل الأنواع السابقة سيطيل المقال وهو خارج موضوع التعريف بلغات البرمجة ويكفي أن تأخذ فكرة على أنواع لغات البرمجة وتتعرف على أشهر أصنافها لأنك عادة عندما تتعلم لغة برمجة يفترض أن تعرف نوعها والصنف الذي تتبع له لأن الأنواع الواحدة تشترك بمفاهيم واحدة تقريبًا يمكن تعلمها بشكل منفصل. ويمكنك الرجوع إلى مقال أنواع لغات البرمجة لتكتشف المزيد من التفاصيل حول معايير وطرق تصنيف وتنظيم لغات البرمجة وشرح مفصل لكل الأنواع التي ذكرناها بالأعلى. لغات برمجة السكربت تُعد لغة برمجة ما أنها لغة سكربت إن كانت: تستخدم مجموعة من التعليمات النصية المكتوبة لتنفيذ أي نوع من العمليات. تعتمد على مضيف: إذ لا يمكن أن تُنتج برامجًا تنفيذية قائمة بحد ذاتها بل ترتبط بنظام تشغيل مثل (سكربتات الطرفيات) أو بيئة عمل (سكربت ويب على الخادم) أو برنامج ( سكربت كتابة ماكرو أو موسّع) أو لغة برمجة أخرى لتنفيذ مجموعة من العمليات التي تهدف إلى تعديل أو تطوير أو زيادة القدرة الوظيفية للمضيف أو تتوسط بينها وبين منظومات أخرى ليشار إليها عندها إلى أنها لغات صمغية glue code. أن تكون لغة مفسّرة وليست مُصرَّفة. تتميز لغات السكربت بكونها، مفتوحة المصدر غالبًا، وسهلة التعليم نسبيًا ولا ضرورة لوجودها ضمن ملف خاص كي تُفسّر، ولا يمكن كتابة تطبيقات أو برامج باستخدامها مباشرة بل تحتاج إلى مضيف لذلك يُقال أنها محمولة. أضف إلى ذلك أنها سريعة التنفيذ كونها تستطيع تنفيذ الطلبات مباشرة (تنفيذ أمر مباشرة) دون الحاجة إلى تعليمات أخرى لتفعيل الطلب كما يزيد سرعتها كونها لغة برمجة مفسّرة. يمكنك مثلًا أن تُنفّذ الأمر ('مرحبًا')window.alert مباشرة في جافاسكربت وهي لغة سكربت مشهورة جدًا، لكنك لن تستطيع فعل ذلك باستخدام لغة ++C. لاحظ ماذا سيتطلب الأمر: // لنقول مرحبًا C++ برنامج بلغة #include <iostream> using namespace std; int main(){ cout<<"مرحبًا"; return 0; } تُصنف لغات برمجة السكربت إلى: سكربتات تطوير الويب: وتستخدم لكتابة صفحات ويب ديناميكية وتطوير مواقع وتطبيقات الويب، ويمكن أن نميز بين نوعين من السكربتات في هذه الصدد سكربتات تعمل من جانب العميل (أي تُستخدم في بناء الواجهة الأمامية للتطبيق أو الموقع) نجد: JavaScript React Next.js سكربتات تعمل من جانب الخادم (أي تستخدم لبناء الواجهة الخلفية للتطبيق، وهي ما يستعرضه المتصفح) نجد منها: JavaScript PHP Node.js ASP.net Ruby Perl Python سكربتات تعمل مع طرفيات أنظمة التشغيل: وتستخدم لتنفيذ الأوامر ضمن واجهات سطر الأوامر في أنظمة التشغيل المختلفة مثل ويندوز ولينكس. من الأمثلة عليها: BASH: في طرفيات لينكس، وتنفذ طيفًا واسعًا من التعليمات مثل الكتابة والقراءة من وإلى الملفات وتنزيل البرمجيات من الإنترنت وتثبيتها وتشغيل البرمجيات وغيرها الكثير. Windows powerShell: في ويندوز، وتنفذ مهامًا مشابهة لما تنفذه طرفية لينكس. سكربتات للأغراض العامة: يمكن أن تُنفذ تقريبًا أي شيء نذكر منها: Ruby Python يمكن استخدام السكربتات في كل المجالات تقريبًا مثل تطبيقات الويب وتطبيقات الهواتف المحمولة وسطح المكتب والتعامل مع الأنظمة وأتمتة المهام على المنظومات السحابية والتنقيب عن البيانات وبرمجة الموّسعات والإضافات إلى البرامج وغيرها الكثير. اللغات التوصيفية Markup Languages يشير مصطلح اللغات التوصيفية markup languages إلى آلية لترميز النصوص عن طريق إضافة مجموعة من الرموز إلى النص تتحكم بتنسيقه أو بإيجاد علاقات بين أقسامه وتسهل عملية أتمتته. إذ تضم اللغات التوصيفية مجموعة من الوسوم أو القواعد التي يشير كل منها إلى دلالة معينة وتستخدم لتنظيم البيانات النصية وعرضها بطريقة يسهل تمييزها بالنسبة للإنسان أو الحاسوب كأن تشير إلى عبارة على أنها عنوان وتبرز عبارة أخرى بكتابتها بخط ثخين وهكذا. لا تُعرض تلك القواعد أو الوسوم ولا تُضاف إلى المحتوى الفعلي بل وظيفتها وصف البيانات فقط وترتيبها. تتطلب هذه اللغات فقط برنامجًا ليحلل الوسوم ويعرض المحتوى وفقًا لمدلول كل وسم وغالبًا ما تحلل المتصفحات أكواد HTML وأكواد XML ثم تعرض النتائج بينما تحلل برمجيات أخرى مخصصة لغات توصيفية أخرى مثل Markdown و DocBook اللتان تستخدمان في المحررات النصية ولاتخ LaTex لكتابة الأوراق البحثية الأكاديمية وغيرها الكثير. لا تحتاج اللغات التوصيفية كلغات البرمجة إلى تصريف Compilation أو تفسير Interpretation كي تحول الشيفرة المكتوبة إلى مجموعة أخرى من التعليمات التي يفهمها الحاسوب، فهي لا تنفذ أية عمليات أو إجرائيات لذلك لا تُعد اللغات التوصيفية لغات برمجة. فكل ما تفعله محللات اللغات التوصيفية هو قراءة الوسوم ومعرفة بدايتها ونهايتها ثم عرض هذه الوسوم بالطريقة الصحيحة تبعًا لدلالة تلك الوسوم. يمكن استخدام هذه اللغات لتصميم صفحات ويب مثل HTML أو لتنسيق صفحات الويب مثل CSS أو لتخزين البيانات مثل XML أو لتنسيق النصوص مثل Markdown وغيرها الكثير. تشرح دورة تطوير واجهات المستخدم من أكاديمية حسوب كل من لغات تطوير واجهات المستخدم الأمامية وهي HTML و CSS وجافاسكربت شرحًا شاملًا بعدد ساعات يزيد عن 50 ساعة فيديو بتطبيقات عملية تطبيقية تناسب سوق العمل. يمكنك الرجوع في هذا الصدد إلى مقال تعلم لغة HTML الذي يتحدث عن اللغات التوصيفية عمومًا ولغة HTML خصوصًا. أسئلة شائعة عن لغات البرمجة إليك بعض الإجابات عن أكثر الأسئلة شيوعًا حول لغات البرمجة. كم عدد لغات البرمجة الموجودة حاليًا؟ عدد لغات البرمجة وفقًا لموقع ويكيبيديا يقارب 700 لغة برمجة مستخدمة أو توقف استخدامها باستثناء اللغات التوصيفية. لكن يرى البعض أنها قد تصل بجميع تصنيفاتها إلى 8000-9000 وهذا رقم مبالغ فيه قليلًا. ما السبب في وجود عدد كبير من لغات البرمجة؟ التنوع نابع عن الحاجة، فلن تتمتع لغة برمجة واحدة بجميع المزايا التي تسهّل العمل في جميع المجالات بفعالية ودقة. قد يكون الأمر مشابهًا لوجود أنواع عدة من المركبات منها السيارة والطائرة والدراجة الهوائية والنارية فلكل استخدامه، إذ يولّد التطور التقني المتسارع الحاجة إلى وجود لغات أكثر مرونة وفعالية في مجالات مختلفة مما يدفع إلى تطوير لغات جديدة تلبي هذه الحاجة. ما هي أحدث لغات البرمجة؟ إذا استثنينا الإصدارات الجديدة من اللغات القديمة مثل الإصدار 3 من لغة Python يمكن أن نجد: لغات البرمجة بالدوال: ELIXIR ELM PURESCRIPT SWIFT لغات البرمجة الإجرائية: Go لغات البرمجة بالكائنات: DART PONY CARBON لغات أغراض عامة: HACK Kotlin NIM RUST ما هي لغات البرمجة السائدة (أشهر لغات البرمجة)؟ إليك أكثر لغات البرمجة استخدامًا وأشهر لغات البرمجة لكن دون نسب مئوية للاستخدام ودون أفضلية لعدم وجود إحصائيات دقيقة وموثوقة. Python JavaScript JAVA C++/C GO Ruby #C PHP أشهر لغة من بينها تُستعمل في بناء تطبيقات سطح مكتب تعمل على مختلف أنظمة التشغيل الحاسوبية هي لغة جافا Java وبايثون Python والأخيرة تستعمل في مجال الذكاء الاصطناعي وتعلم الآلة والتعامل مع البيانات وغيرها، أما في مجال الويب، فأشهر لغة فيه حاليًا هي لغة جافاسكربت JavaScript فيمكنك باستعمالها تطوير الواجهات الأمامية Frontend وأصبح بإمكانك أيضًا تطوير الواجهات الخلفية Backend عبر بيئة Node.js وكما يمكنك باستعمال تقنيات الويب تطوير تطبيقات لسطح المكتب تعمل على كل أنظمة التشغيل باستعمال إطار العمل Electron.js، وتعد دورة تطوير التطبيقات باستخدام لغة JavaScript أفضل مرجع عربي يشرح لغة جافاسكربت وكافة تطبيقاتها لبناء تطبيقات الويب وتطبيقات سطح المكتب شرحًا عمليًا. ما هي أسهل لغات البرمجة من حيث التعلم والاستخدام؟ لا يمكن أن نجد إجابة محددة عن سؤال أسهل لغات البرمجة فالغرض من استخدامك للغة البرمجة وتآلفك معها مسألة حاجة ورغبة: PHP Python JavaScript Ruby C/C++/JAVA Kotlin Swift عمومًا، يقال عن لغة بايثون أنها أسهل لغة برمجة يمكن البدء بتعلمها والسبب أنها صياغة اللغة قريبة جدًا من صياغة اللغة الإنجليزية فعندما تكتب شيفرة برمجية كأنك تصيغ فقرة من لغة إنجليزية وهذا ما يميزها عن بقية اللغات التي تكثر فيها الأحرف والكلمات الغامضة وعلامات الترقيم والأقواس المتناثرة هنا وهناك، ولهذا السبب تبدأ أغلب الجامعات بشرح هذه اللغة كأول لغة برمجة لطلاب علوم الحاسوب وتبدأ أغلب الدورات والكورسات البرمجة بشرحها لمن يريد تعلم البرمجة بمفرده، وأفضل مرجع عربي شامل متكامل من تلك الدورات هي دورة تطوير التطبيقات باستخدام لغة Python من أكاديمية حسوب. هل عليَّ تعلم لغة برمجة؟ إن كنت تنوي تطوير مهنتك الهندسية أو العلمية أو التسويقية أو الاقتصادية أو التحليلية فالجواب نعم بكل تأكيد. ما الفائدة من تعلم لغة برمجة؟ تطوير مسيرتك المهنية، فكل الدلائل تشير إلى أننا مقبلون وبشدة على عصر الآلات الذكية. الاطلاع على الطريقة التي تعمل بها الآلات المبرمجة مما قد يساعدك على استخدامها بالشكل الصحيح. تحسين قدراتك العقلية على التحليل وترتيب الأفكار. إضافة إلى تلك الأسباب العامة، ستجد من مهنة المبرمج أو مطور ويب مهنة ممتعة ومجزية ماديًا فالطلب يزداد بشكل كبير على المنتجات الرقمية وتتنوع مجالات استخداماتها لما تقدمه من فوائد على جميع الأصعدة مثل أتمتة المهام المختلفة وتحليل البيانات والحسابات الرياضية والتقنية المتقدمة وإدارة المتاجر الإلكترونية والأعمال وغير ذلك الكثير. فالبرمجة هي مهنة المستقبل بامتياز، وأيًا كانت مهنتك الأصلية ستجد البرمجة سبيلًا إليها، وهكذا ستجني فائدة مضاعفة بتعلمك إحدى لغات البرمجة. هل من الصعب تعلم لغة برمجة؟ الجواب لا إن كنت تمتلك الإرادة والمثابرة. تعلم البرمجة شبيه بركوب الدراجة ما أن تُصِرَّ على تعلمها ثم تتعلم كيف تستخدمها لن تنسى أبدًا كيف ستقودها وكذلك لن تنسى كيف تفكر برمجيًا. لكن إن أردت أن تصبح محترفًا، فهذا طريق طويل تعترضك فيه بعض الصعوبات. ما هي مجالات استخدام لغات البرمجة؟ من كتابة كلمة "مرحبًا" على شاشة حاسوبك إلى التحكم بمسبار فضائي يحط على كوكب آخر. فكل ما تعرضه لك الأجهزة الإلكترونية من بسيطها إلى أعقدها هي نتاج عملية برمجة لهذه الأجهزة. ويمكن تلخيص استخدامات لغات البرمجة وفق الخطوط العريضة التالية: تطوير تطبيقات للحواسب تطوير تطبيقات للويب تطوير تطبيقات للهواتف الذكية برمجة الآلات والتجهيزات القابلة للبرمجة تطوير وإدارة الخدمات السحابية تطوير أنظمة تشغيل تطوير أنظمة وآلات ذاتية التعلم مجالات الذكاء الاصطناعي والروبوتات احصل على موقع إلكتروني مخصص لأعمالك أبهر زوارك بموقع احترافي ومميز بالاستعانة بأفضل خدمات تطوير وتحسين المواقع على خمسات أنشئ موقعك الآن خاتمة تعرفنا في هذه المقال على لغات البرمجة التي نستخدمها في برمجة الحواسب والأجهزة الإلكترونية الذكية أو القابلة للبرمجة واستعرضنا الخطوط العامة لمكوّنات أي لغة برمجة ثم فصلنا في أنواعها وقلنا أنها قد تكون عامة الغرض أو مخصصة من ناحية الاستخدام أو لغات مكتوبة أو مرئية من ناحية إظهار الشيفرة وأنها مفسّرة أو مصرّفة من ناحية معالجة المعلومات. كما ذكرنا أن لغات البرمجة التي تقترب تعليماتها وظيفيًا من التعليمات التي يفهمها الحاسوب هي لغات منخفضة المستوى ويزداد مستواها باستخدامها تعليمات تنفذ وظائف أكبر من تعليمات الحاسوب الأساسية. ومررنا أخيرًا على مفهوم لغات السكربت واستخداماتها بشيء من التفصيل، وعلى مفهوم اللغات التوصيفية واختلافها عن لغات البرمجة. وبهذا الشكل نكون قد أحطنا ولو بالشيء اليسير بمفاهيم هذا العالم الواسع لتكون خطوتك الأولى في الإنطلاق إلى عالم البرمجة. اقرأ أيضًا المدخل الشامل لتعلم علوم الحاسوب كيف تتعلم البرمجة فوائد تعلم البرمجة دليلك إلى: لغات برمجة الألعاب المدخل الشامل لتعلم تطوير الويب وبرمجة المواقع المرجع الشامل إلى تعلم لغة بايثون البرمجة بلغة جافاسكربت مقارنة بين JavaScript و TypeScript مقارنة بين PHP و NodeJS
  20. تُعَدّ HTML أو لغة توصيف النصوص التشعبية بأنها الشيفرة التي تُستخدَم في هيكلة صفحات ويب ومحتوياتها، فقد تُنظَّم الصفحة مثلًا على هيئة مجموعة من المقاطع النصية أو قائمة من النقاط أو أن تعرض الصور أو جداول البيانات، وسنقدِّم لك في هذا المقال مجموعة معارف أساسية لفهم لغة HTML ووظائفها. ما هي HTML؟ تُعَدّ HTML بأنها لغة توصيفية تُعرِّف هيكلية المحتوى الذي تقدِّمه الصفحة، وتتألف اللغة من سلسلة من العناصر التي تستخدِمها لتضمين أو تغليف الأجزاء المختلفة من المحتوى لتظهر بطريقة محددة أو لتعمل بطريقة محددة، كما يمكن أن تربط الوسوم المغلقة enclosing tags كلمةً أو صورةً بمكان آخر، أو يمكنها عرض الكلمات عرضًا مائلًا أو تكبّر خط الكتابة أو تصغّره وهكذا، وإليك على سبيل المثال المحتوى التالي الذي لا يتعدى السطر: My cat is very grumpy إذا أردنا أن يظهر السطر كما هو، فيمكننا أن نجعله فقرةً نصيةً بتضمينه داخل وسمَي فقرة، أي <p> </p>: <p>My cat is very grumpy</p> تشريح عنصر HTML دعونا نستكشف عنصر الفقرة السابق بصورة أعمق: إن الأجزاء الرئيسية من العنصر هي: وسم البداية Opening tag: يشير هذا الوسم إلى النقطة التي يبدأ عندها العنصر أو التي يبدأ تأثيره عندها (بداية الفقرة النصية في حالتنا)، ويتكوّن من اسم العنصر (p في حالتنا) محاطًا بقوسَي زاوية. وسم النهاية Closing tag: يشير هذا الوسم إلى نهاية العنصر (نهاية الفقرة في حالتنا)، ويشبه وسم البداية لكنه يبدأ بشرطة أمامية / قبل اسم العنصر، كما يُعَدّ إغفال وسم النهاية من أكثر الأخطاء التي يرتكبها المبتدئون، وقد تفضي إلى نتائج غريبة بالفعل. المحتوى Content: يشير إلى المحتوى الفعلي للعنصر، وهو في حالتنا مجرد نص. العنصر Element: ويتكون من وسمَي البداية والنهاية والمحتوى معًا. يمكن أن تمتلك العناصر سمات attributes تبدو شبيهةً كما يلي: تتضمن السمات معلومات إضافية عن العنصر لا تريدها أنت أن تظهر مثل جزء من المحتوى الفعلي، فالكلمة class في الشكل السابق هي اسم السمة وقيمتها هي editor-note، إذ تسمح لك السمة class بإعطاء العنصر معرِّفًا عامًا يمكن استخدامه لتطبيق معلومات تنسيق على هذا العنصر أو أي عنصر تحمل السمة class فيه القيمة نفسها أو غيرها. لكتابة السمة لا بد من: مسافة فارغة بينها وبين اسم العنصر، أو بينها وبين السمة التي تسبقها في حال امتلك العنصر سمتَين أو أكثر. اسم السمة تليها إشارة المساواة =. قيمة السمة بين إشارتَي تنصيص. ملاحظة: يمكنك عدم وضع قيمة السمة بين إشارتي تنصيص إذا لم تتضمن رمز ASCII الخاص بالمسافة الفارغة أو أيّ من المحارف " أو ' أو ` أو = أو < أو >، لكن يفضَّل وضع قيمة السمة دائمًا داخل إشارتَي التنصيص لأنها تجعل الشيفرة واضحةً ومفهومةً. دورة تطوير واجهات المستخدم ابدأ عملك الحر بتطوير واجهات المواقع والمتاجر الإلكترونية فور انتهائك من الدورة اشترك الآن العناصر المتداخلة يمكن وضع عناصر داخل عناصرأخرى أيضًا وهذا ما يُعرف بالتداخل nesting، فإذا أردنا أن تظهر الكلمة "very" في الفقرة "My cat is very grumpy" بخط سميك، فيمكن تغليف هذه الكلمة داخل العنصر <strong></strong> كما يلي: <p>My cat is <strong>very</strong> grumpy.</p> لكن عليك التأكد دومًا من أنّ العناصر متداخلة بصورة صحيحة، فقد فتحنا في المثال السابق العنصر <p> أولًا ثم <strong>، وبالتالي توجَّب إغلاق العنصر الثاني آخر من فتح على الشكل <strong/> ثم إغلاق الأول <p/>، أي أنّ التداخل التالي غير صحيح: <p>My cat is <strong>very grumpy.</p></strong> لا بد من فتح وإغلاق العناصر بالصورة الصحيحة لكي تظهر بوضوح داخل أو خارج عنصر آخر، فإذا تداخلت بالشكل الذي عرضناه في الشيفرة السابقة، فسيحاول المتصفح أن يخمّن بأفضل شكل ما تحاول قوله، مما قد يتسبب بظهور نتائج غير متوقعة، فلا تفعل ذلك. العناصر الفارغة تُدعى بعض العناصر التي لا تحمل أيّ محتوى بالعناصر الفارغة Empty elements مثل العنصر <img> الذي استخدمناه سابقًا: <img src="images/firefox-icon.png" alt="My test image"> إذ يمتلك العنصر سمتَين ولا يمتلك وسم نهاية <img/> أو محتوى، والسبب أن عنصر الصورة لا يغلِّف محتوًى لكي يتأثر بوجود أو عدم وجود وسم النهاية، فوظيفته هي إدراج صورة في صفحة HTML في المكان الذي يظهر فيه. تشريح مستند HTML سنشرح الآن الطريقة التي نجمّع بها عناصر HTML المفردة لنشكِّل صفحةً كاملةً، فلنعُد قليلًا إلى الشيفرة التي وضعناها في الملف index.html والذي رأيناه أول مرة في المقال السابق: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>My test page</title> </head> <body> <img src="images/firefox-icon.png" alt="My test image"> </body> </html> لدينا هنا الأشياء التالية: <DOCTYPE html!>: وهو عنصر تمهيدي مطلوب، فقد كانت الغاية من هذا العنصر في الأيام الأولى (1991/1992) أن يعمل على أساس رابط إلى مجموعة من القواعد التي ينبغي أن تحققها صفحة HTML لكي تُعَدّ صفحةً جيدةً بما في ذلك الاكتشاف التلقائي للأخطاء وغيرها من النقاط المفيدة، وليس لهذا العنصر في أيامنا هذه وظيفةً سوى التأكد من سلوك المستند للسلوك المطلوب، وهذا كل ما عليك معرفته حاليًا. <html></html>: يضم هذا العنصر كامل محتوى الصفحة، ويُدعى أحيانًا بالعنصر الجذري root element. <head></head>: يعمل على أساس حاوية لتضع فيها كل الأشياء التي تريدها في صفحتك ولكنها لا تمثِّل محتوًى تريد إظهاره لمتابعيك مثل الكلمات المفتاحية ووصف الصفحة الذي تريد إظهاره عندما يعرضها محرك البحث على أساس نتيجة وقواعد CSS لتنسيق محتوى الصفحة ونوع المحارف التي تستخدمها في الصفحة وغيرها الكثير. <"meta charset="utf-8>: يضبط هذا العنصر مجموعة المحارف التي تستخدِمها في الصفحة، وهنا اخترنا المجموعة UTF-8 التي تضم محارف الأغلبية الساحقة من اللغات المكتوبة، إذ تستطيع هذا المحارف أن تعرض الآن أيّ محتوى نصي بأيّ لغة قد تضعه في صفحتك، ولا مبرر لعدم ضبط مجموعة المحارف المستخدَمة، كما ستساعدك على تحاشي الكثير من الأخطاء لاحقًا. <title></title>: يضبط هذا العنصر عنوان صفحتك الذي يظهر أعلى المتصفح عند تحميل الصفحة، كما يُستخدم لوصف الصفحة عندما تضيفها إلى قائمة الصفحات المفضلة. <body></body>: يضم المحتوى الذي تريد عرضه على زائري صفحتك بأكمله، سواءً كان نصًا أو صورًا أو فيديو أو ألعاب أو أيّ شيء آخر. الصور لنعُد إلى العنصر <img> مجددًا: <img src="images/firefox-icon.png" alt="My test image"> يُدرِج هذا العنصر صورةً ضمن الصفحة في المكان الذي تضعه فيه، إذ نحدِّد الصورة المعروضة بكتابة عنوانها على أساس قيمة للسمة src -أي مصدر source-، كما يمتلك عنصر الصورة أيضًا سمةً أخرى تُدعى alt -أي بديل alternative-، إذ تُستخدَم هذه السمة لعرض نص بديل عن الصورة للأشخاص الذين لا يستطيعون رؤيتها لأسباب عديدة منها: المشاكل البصرية: إذ يستخدِم الكثيرون من فاقدي البصر أدوات تُدعى قارئات الشاشة تستطيع قراءة النص البديل. أخطاء في عرض الصورة: حاول مثلًا تغيير المسار الموجود داخل السمة في مثالنا السابق ثم احفظ الملف وأعد تحميله ضمن المتصفح، إذ لن تظهر الصورة، وإنما نص كما يلي: ما يهم فعلًا في النص البديل هو أن يكون وصفيًا تمامًا لمحتوى الصورة، إذ ينبغي أن يزوِّد النص القارئ بمعلومات كافية ليكوِّن فكرةً جيدةً عن محتوى الصورة، فالنص البديل "My test image" في مثالنا غير جيد على الإطلاق، وسيكون الأنسب لشعار فايرفوكس هو "Firefox logo: a flaming fox surrounding the Earth" وبالعربية "شعار فايرفوكس: ثعلب ملتهب يحيط بالكرة الأرضية"، أي حاول إذًا ابتكار نصوص بديلة جيدة تصف الصورة. توصيف محتوى صفحات HTML يغطي هذا القسم من المقال عناصر HTML الأساسية لتوصيف محتوى الصفحات. العناوين تساعدك العناوين Headings على عرض أجزاء من محتوى الصفحة على أساس عناوين رئيسية أو فرعية، وكما هو حال الكتب التي تحمل عنوانًا رئيسيًا وعناوين للفصول وعناوين فرعية في كل فصل، تحمل صفحة HTML الميزة ذاتها، إذ تضم اللغة عناوين تتدرج إلى مستويات ستة من <h1> إلى <h6>، وغالبًا ما ستستخدِم ثلاث إلى أربع مستويات كحد أقصى. <!-- 4 heading levels: --> <h1>My main title</h1> <h2>My top level heading</h2> <h3>My subheading</h3> <h4>My sub-subheading</h4> ملاحظة: كل ما يرد بين الوسمين <-- و --!> في صفحة HTML هو تعليق سيتجاهل المتصفح محتواه عندما يصيّر الشيفرة ولن يُعرَض على الشاشة، إذ تساعدك التعليقات في وضع ملاحظات مفيدة عن مقطع محدَّد من الشيفرة أو عن منطق معيّن اعتمدته. حاول الآن وضع عنوان فرعي في صفحة HTML التي نبنيها فوق العنصر <img> مباشرةً. ملاحظة: تملك العناوين من المستوى الأول تنسيقًا ضمنيًا خاصًا، لذلك لا تستخدِمها لتكبير النص أو جعله سميكًا Bold لأنها تُستخدَم لغايات أخرى مثل سهولة الوصول أو لأسباب أخرى مثل تحسين محركات البحث، لذا حاول أن تعطي تدرجًا منطقيًا للعناوين في صفحتك دون تجاوز أيّ مستوى إلى ما دونه مثل الانتقال من 4 إلى 2 مباشرةً دون المرور بالمستوى 3. الفقرات النصية يحتوي العنصر <p> كما شرحنا سابقًا مقطعًا نصيًا، وستستخدمه بكثرة عندما ترمز المحتوى النصي في الصفحة: <p>This is a single paragraph</p> يمكنك على سبيل التجربة إضافة عينة من نص ما على هيئة فقرة نصية أو أكثر تحت العنصر <img> في الصفحة التجريبية التي نبنيها. القوائم تُصنَّف كمية لا بأس بها من محتوى ويب على صورة قوائم Lists وتمتلك HTML القدرة على ذلك، إذ تتضمن القائمة عنصرين على الأقل، وأكثر القوائم شيوعًا هي القوائم المرتبة Ordered وغير المرتبة Unordered: القوائم المرتبة: توضع بنود هذه القائمة ضمن العنصر <ol>، وتُستخدَم لعرض القوائم التي يهمنا فيها ترتيب العناصر مثل وصفات تحضير الطعام أو ترتيب فِرق دوري لكرة القدم وغيرها. القوائم غير المرتبة: توضع بنود هذه القائمة ضمن العنصر <ul>، وتُستخدَم لعرض القوائم التي لا نهتم فيها لترتيب العناصر مثل قائمة التسوق. يوضع كل بند من بنود القائمة ضمن العنصر <li> -أي بند من قائمة list item-، فإذا أردنا مثلًا تحويل جزء من الفقرة النصية التالية إلى قائمة: <p>At Mozilla, we’re a global community of technologists, thinkers, and builders working together ... </p> فيمكننا تعديل توصيف محتوى الفقرة ليصبح كما يلي: <p>At Mozilla, we’re a global community of</p> <ul> <li>technologists</li> <li>thinkers</li> <li>builders</li> </ul> <p>working together ... </p> الروابط تُعَدّ الروابط العنصر الأكثر أهميةً، فهي ما تجعل من الويب شبكةً حقيقيةً، إذ نستخدِم العنصر البسيط <a> لإنشاء رابط، وهو اختصار للكلمة "anchor"، ولتجعل جزءًا من الفقرة النصية السابقة رابطًا اتبع الخطوات التالية: اختر جزءًا من النص وليكن "Mozilla Manifesto". ضَع هذا الجزء ضمن الوسمَين <a></a> كما يلي: <a>Mozilla Manifesto</a> زوّد الرابط بعنوان للانتقال إليه من خلال السمة href كما يلي: <a href="">Mozilla Manifesto</a> اكتب العنوان المطلوب بين علامَتي تنصيص السمة href كما يلي: <a href="https://www.mozilla.org/en-US/about/manifesto/">Mozilla Manifesto</a> قد تصل إلى نتيجة غير متوقعة إذا حذفت بداية العنوان الذي يُدعى بروتوكول، أي //:https أو ://http، لذلك تحقق من وصولك إلى المكان المطلوب عند النقر على الرابط. ملاحظة: قد يبدو اختيار اسم السمة href غامضًا في البداية، فإذا صعُب عليك الأمر، تذكر أنه مشتق من كلمتَي "مرجع إلى نص تشعبي hypertext reference" خلاصة إذا اتبعت التوجيهات التي أشرنا إليها خلال اطلاعك على هذا المقال، فستبدو صفحة الويب التي نبنيها بالشكل التالي تقريبًا إذا لم تغير في العناوين أو النصوص. إذا لم تجد عملك صحيحًا، فيمكنك دائمًا موازنة ما فعلته مع النسخة الجاهزة على جيت-هاب. ترجمة -وبتصرف- للمقال HTML Basics. اقرأ أيضًا المقال السابق: التعامل مع الملفات في عملية تطوير موقع الويب توثيق لغة HTML العربي مكونات الويب: عناصر HTML المخصصة وقوالبها دليل: تعلم لغة HTML
  21. تتمتع البرمجيات بدورة حياة كاملة ابتداءً من التصورات الأولية وانتقالًا إلى البرمجة ومنها إلى إصدار البرنامج إلى المستخدم النهائي وصيانته. سنتعرف في هذا المقال على مفهوم الحاويات Containers، وهي أداةٌ عصرية تُستخدم في المراحل النهائية من دورة حياة البرنامج. تُغلّف الحاويات تطبيقك ضمن حزمةٍ واحدة تتضمن كل الاعتماديات التي يحتاجها التطبيق، وتكون النتيجة حاويةً يمكن أن تعمل بصورةٍ مستقل عن غيرها من الحاويات. تمنع الحاويات التطبيق من الوصول واستخدام ملفات وموارد الأجهزة، وقد يعطي مطورو التطبيقات بعض الأذونات للبرنامج الذي تضمه الحاوية في الوصول إلى ملفات محددة وتخصيص موارد محددة أيضًا. ولنكون أكثر دقة، تُعدّ الحاويات بمثابة بيئات عمل افتراضية على مستوى نظام التشغيل، وأكثر ما يشابهها من الناحية التقنية هي الآلات الافتراضية Virtual machines التي تساعد على تشغيل عدة أنظمة تشغيل على جهاز فيزيائي واحد. وينبغي على الآلات الافتراضية تشغيل النظام بأكمله، بينما تُشغّل الحاوية التطبيق بالاستفادة من نظام التشغيل الذي يستضيفها. وهكذا سيكون الاختلاف بين الحاويات والآلات الافتراضية هي أنك لن تلاحظ إلا نادرًا جدًا زيادة في تحميل النظام عند استخدام الحاويات، فهي عادةً ما تحتاج إلى تنفيذ عملية واحدة. يمكن للحاويات أن تكون سريعةً وقابلةً للتوسع لكونها خفيفة الوزن موازنةً بالآلات الافتراضية على الأقل، وطالما أنها تعزل التطبيق الذي يُنفَّذ ضمنها، سيساعد ذلك في الحصول على نفس الأداء تمامًا وفي أي مكان. وبالنتيجة، تُعد الحاويات خيارًا ممتازًا في أي بيئة تشغيل سحابية أو للتطبيقات التي يكثر مُستخدميها. تدعم معظم الخدمات السحابية، مثل AWS و Google Cloud و Microsoft Azure الحاويات وبأشكال متعددة، بما في ذلك AWS Fargate و Google Cloud Run وهما خدمتان لتشغيل الحاويات دون خادم serverless إذ لا حاجة مطلقًا لتنفيذ الحاوية إن لم تُستخدم. يمكن أيضًا تثبيت مقومات تشغيل البيئة على أغلب الأجهزة وتشغيل الحاويات بنفسك، بما في ذلك حاسوبك الخاص. طالما أن الحاويات تعمل في البيئات السحابية وحتى أثناء تطوير التطبيقات، فما الفائدة من الفعلية منها إذًا؟ إليك حالتي الاستخدام التاليتين: الحالة الأولى: عندما تحاول أن تطور تطبيقًا من المفترض أن يعمل على نفس الجهاز لدعم إصدار أقدم، فكلاهما يحتاج إلى تثبيت نسخ مختلفة من Node.js. بإمكانك استخدام nvm أو آلة افتراضية أو حتى اختراع طريقة سحرية لجعل النسختين تعملان معًا، لكن تبقى الحاويات حلًا ممتازًا لأنك تستطيع تشغيل كلا التطبيقين كلًا ضمن حاويته الخاصة، فهما معزولان عن بعضهما ولا يتداخلان. الحالة الثانية: عندما يعمل تطبيقك على حاسوبك وتحاول نقله إلى خادم، فمن الأمور الشائعة ألا يعمل التطبيق على الخادم علمًا أنه يعمل جيدًا على حاسوبك. قد يكون الأمر اعتماديات مفقودة، أو اختلافات أخرى في بيئات التشغيل. في حالة كهذه، تظهر الحاويات مثل حل رائع، لأنها قادرة على تنفيذ التطبيق في نفس بيئة التنفيذ على حاسوبك أو على الخادم. وقد لا يكون حلًا مثاليًا نظرًا لاختلاف العتاد الصلب، لكن يمكنك أن تجعل الاختلافات في بيئات التنفيذ محدودةً جدًا. قد تسمع أحيانًا عبارات من قبيل "يعمل ضمن حاويتي"، إذ تصف هذه العبارة الحالة التي يعمل فيها التطبيق جيدًا ضمن حاوية على جهازك لكنها تُخفق في تنفيذ التطبيق عندما تنقلها إلى الخادم. هذه العبارة هي تلاعب في العبارة "يعمل على جهازي"، فالحاويات مصممةٌ لتعمل، وغالبًا ما تكون هذه الحالة خطأ في الاستخدام. حول هذا القسم لن نركز اهتمامنا في هذا القسم على شيفرة جافاسكربت JavaScript، بل تهيئة البيئة التي سيُنفَّذ فيها التطبيق. لهذا قد لا تحتوي التمارين أية شيفرات. كما ستجد التطبيق جاهزًا على غيت هب GitHub ويحتاج فقط إلى التهيئة. ينبغي رفع التمارين إلى مستودع GitHub واحد يضم كل الشيفرات المصدرية وإعدادات التهيئة التي نفّذتها في هذا القسم. لا بد أن تمتلك معرفة مبدئية بالتعامل مع Node و Express و React، وعليك إكمال الأقسام الجوهرية من 1 إلى 5 للمتابعة في هذا القسم. التمرين 12.1 تنبيه سنخرج في هذا القسم من دائرة الراحة الخاصة بمطوري JavaScript، وقد يتطلب منك ذلك جولةً لتتآلف مع أوامر الصدفة shell أو سطر الأوامر command line أو محث الأوامر command prompt أو الطرفية terminal قبل أن تبدأ؛ فإذا كنت تستخدم واجهة مستخدم رسومية ولم تلمس أبدًا طرفية لينوكس Linux أو ماك Mac، وشعرت بأنك تائه لا تعرف كيف تبدأ حل التمرين، ننصحك بالاطلاع على القسم الأول من كتاب "Computing tools for CS studies"، فهو يضم كل ما تحتاجه لمتابعة العمل هنا. التمرين 12.1: استخدام الحاسوب دون واجهة مستخدم رسومية الخطوة الأولى: اقرأ النص الذي يلي التنبيه. الخطوة الثانية: نزّل مستودع التمرين واجعله مستودع تسليم التمارين لهذا القسم. الخطوة الثالثة: انتقل إلى العنوان "http://helsinki.fi" واحفظ الخرج ضمن ملف، ثم احفظ الملف ضمن مستودعك بالاسم "script-answers/exercise12_1.txt". وتذكر أنك أنشأت بالفعل المجلد "script-answers" في الخطوة السابقة. أدوات العمل تحتاج إلى بعض الأدوات الأساسية التي تختلف وفقًا لنظام التشغيل. ستحتاج WSL 2 terminal على نظام ويندوز. الطرفية على نظام ماك. سطر الأوامر على نظام لينوكس. تثبيت كل ما تحتاجه للعمل سنبدأ بتثبيت البرامج التي نحتاجها، وقد تشكل هذه الخطوة إحدى العقبات المحتملة، إذ لا بد من أذونات واسعة النطاق على جهازك طالما أننا نتحدث عن شكلٍ من البيئات الافتراضية على مستوى نظام التشغيل، وهذا ما يتطلب الولوج إلى نواته. تتركز مادة القسم حول دوكر Docker، وهو يمثّل مجموعةً من المنتجات التي سنستخدمها في تشكيل وإدارة الحاويات، لكن إن لم تستطع تثبيته فلن تتمكن لسوء الحظ من متابعة العمل في هذا القسم. ستختلف إرشادات عملية تثبيت المنتجات على حاسوبك وفقًا لنظام التشغيل، لذلك عليك أن تجد الإرشادات الصحيحة لتثبيت المنتج من خلال صفحة Docker المخصصة للغرض، وتذكر وجود خيارات عدة للتثبيت على نفس نظام التشغيل. لنفترض أن كل شيء جرى على ما يُرام، لهذا سنتأكد من تطابق الإصدارات، إذ لا بُد أن يكون إصدار نسختك أعلى من هذا الإصدار: $ docker -v Docker version 20.10.5, build 55c4c88 الحاويات والصور هناك مفهومان جوهريان فيما يتعلق بالحاويات، ومن السهل الخلط بينهما: الحاوية والصورة؛ إذ أن الحاوية هي نسخة التشغيل من الصورة، لهذا فكلا العبارتين التاليتين صحيحة: تضم الصور الشيفرات والاعتماديات والإرشادات اللازمة لتشغيل التطبيق. تُحزّم الحاويات البرنامج ضمن وحدات معيارية. وبالتالي لا عجب من سهولة الخلط بينهما. للمساعدة في تجاوز الأمر، تُستخدم كلمة "حاوية" للإشارة إلى كلا المفهومين. لكنك لن تتمكن أبدًا من بناء حاوية أو تنزيلها لأنها موجودةٌ فقط أثناء التشغيل؛ أما الصورة فهي ملفاتٌ غير قابلة للتغيير، وبالتالي لن تكون قادرًا على تعديل صورة بعد إنشائها. لكن يمكنك استخدام صورة موجودة لإنشاء صورة جديدة بإضافة طبقات جديدة فوق القديمة. وبعبارات مجازية مستوردة من عالم الطبخ: الصورة هي وجبة مطبوخة مسبقًا ومجمّدة. الحاويات هي تلك الوجبة اللذيذة الجاهزة للأكل. يُعد دوكر Docker أكثر تقنيات إنشاء الحاويات شهرةً وقد أبدع المعايير التي تستخدمها معظم تقنيات إنشاء الحاويات حاليًا. هذا المنتج تقنيًا هو مجموعة منتجات تساعد في إدارة الصور والحاويات وتمنحنا القدرة على العمل مع كل ميزاتها، إذ سيتولى Docker مثلًا مهمة تحويل الصور إلى حاويات. هناك أداةٌ تُدعى Docker Compose لإدارة حاويات دوكر، إذ تسمح لك بتنسيق العمل على عدة حاويات في نفس الوقت، لهذا سنستخدمها في هذا القسم لبناء بيئة تطوير محلية مركّبة، ولم يعد هناك حاجةٌ لنثبت Node عند الانتهاء من إعداد بيئة التطوير هذه. يبقى هناك مجموعةٌ من المصطلحات التي علينا أن نعرّج عليها، لكننا سنتجاهل ذلك مؤقتًا لنتعلم أكثر عن دوكر. لنبدأ من الأمر docker container run الذي يُستخدم لتشغيل الصور ضمن حاوية. لهذا الأمر الهيكلية التالية: container run *IMAGE-NAME* التي تخبر دوكر بإنشاء حاويةٍ من الصورة. ومن الميزات الجيدة لهذا الأمر هو إمكانية تشغيل حاوية حتى لو لم تُنزّل الحاوية على الجهاز بعد. لننفِّذ الأمر: § docker container run hello-world ستكون النتيجة العديد من المُخرجات، لهذا سنحاول فصلها إلى عدة أقسام كي نفسر هذه المخرجات معًا. رقمنا الأسطر لسهولة شرح ما فيها، فلن تجد أية أرقام لأسطر الخرج في الواقع: 1. Unable to find image 'hello-world:latest' locally 2. latest: Pulling from library/hello-world 3. b8dfde127a29: Pull complete 4. Digest: sha256:5122f6204b6a3596e048758cabba3c46b1c937a46b5be6225b835d091b90e46c 5. Status: Downloaded newer image for hello-world:latest لم يعثُر الأمر على الصورة في جهازك، لذلك نزّلها من مُسجّل مجاني يُدعى Docker Hub. بإمكانك الاطلاع على صفحة الويب الخاصة بهذا المسجّل باستخدام المتصفح. تنص الرسالة الأولى أن الصورة "hello-world:latest" غير موجودة بعد، وهذا ما يكشف بعض التفاصيل عن الصور بحد ذاتها، إذ تتألف أسماء الصور من عدة أجزاء بصورةٍ مشابهة لعناوين URL وهي على الشكل: registry/organisation/image:tag في حالتنا يعوَّض عن الحقول الثلاثة المفقودة بالقيم الافتراضية: index.docker.io/library/hello-world:latest يعرض السطر الثاني اسم المنظمة والمكتبة التي تحصل على الصورة منها، ويُختصر عنوان المكتبة في Docker Hub إلى _. يعرض السطران الثالث والخامس الحالة فقط، لكن ما يضمه السطر الرابع هو المهم. لكل صورة معرّف تعمية مختزل وفريد digest بناءً على الطبقات التي بُنيت منها الصورة. وفي واقع الأمر تُنشئ كل تعليمة أو خطوة استُخدمت في بناء الصورة طبقةً فريدة، لهذا يستخدم دوكر معرّف التعمية المختزل لتمييز أن الصورة بقيت كما هي. وهكذا نرى أن خرج الأمر السابق هو سحب ثم إخراج معلومات عن الصورة. تخبرنا الحالة بعد ذلك أن نسخةً جديدةً من الصورة "hello-world:latest" قد نُزّلت بالفعل. بإمكانك سحب الصورة باستخدام الأمر docker image pull hello-world ومتابعة ما سيحدث. يمثل التالي خرجًا ناتجًا عن حاوية، ويشرح ما يجري عند تنفيذ الأمر docker container run hello-world: Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal. To try something more ambitious, you can run an Ubuntu container with: $ docker container run -it ubuntu bash Share images, automate workflows, and more with a free Docker ID: https://hub.docker.com/ For more examples and ideas, visit: https://docs.docker.com/get-started/ يتضمن الخرج بعض الأشياء الجديدة التي يجب تعلّمها مثل: Docker daemon: وهي خدمة في الخلفية تتحقق من عمل الحاوية. Docker client: واجهة للتفاعل مع الخدمة السابقة. لقد تفاعلنا مع الصورة الأولى وأنشأنا حاوية من هذه الصورة، وتلقينا الخرج أثناء تنفيذ الحاوية. التمرين 12.2 لا تتطلب منك بعض هذه التمارين كتابة أية شيفرات أو إعدادات تهيئة في ملف. عليك أن تستخدم في هذا التمرين الأمر script لتسجيل الأوامر التي استخدمتها. جرّب ذلك بتنفيذ التعليمة script لتبدأ التسجيل ثم الأمر "echo "hello لتوليد خرج ما، ومن ثم التعليمة exit لإيقاف التسجيل. تُسجِّل تلك التعليمات أفعالك في ملف اسمه "typescript". يمكنك أيضًا نسخ ولصق جميع الأوامر التي استخدمتها في ملف نصي إن لم تستطع استخدام script. التمرين 12.2 تشغيل حاويتك الثانية استخدم script لتسجيل ما تفعله واحفظ الملف بالاسم "script-answers/exercise12_2.txt". نفّذ ما يلي: الخطوة الأولى: شغّل حاوية بنفس أسلوب تشغيل الحاوية، إذ ستربطك هذه الخطوة مباشرةً مع الحاوية عبر تعليمات bash وستكون قادرًا على الوصول إلى كل الملفات والأدوات الموجودة ضمن الحاوية. لهذا نفذ الخطوات التالية ضمن الحاوية. الخطوة الثانية: أنشئ المجلد "usr/src/app/". الخطوة الثالثة: أنشئ الملف "usr/src/app/index.js/". الخطوة الرابعة: نفّذ التعليمة exit للخروج من الحاوية. ابحث عن طريقة إنشاء الملفات أو المجلدات بمساعدة محركات البحث إن لزم الأمر. صورة Ubuntu يحتوي الأمر التالي المُستخدم في تشغيل حاوية "ubuntu": docker container run -it ubuntu bash بعض الإضافات عن أمر تشغيل الحاوية "hello-world". سنستخدم التعليمة help-- لفهم الموضوع أكثر، وسنجتزئ بعض المعلومات التي تتعلق بحديثنا مما يرد في خرج العملية: $ docker container run --help Usage: docker container run [OPTIONS] IMAGE [COMMAND] [ARG...] Run a command in a new container Options: ... -i, --interactive Keep STDIN open even if not attached -t, --tty Allocate a pseudo-TTY ... تتحقق الرايتان أو الخياران it- من قدرتنا على التفاعل مع الحاوية، ثم نحدد بعد ذلك أن الصورة التي نشغلها هي "ubuntu"، ثم لدينا الأمر bash الذي يُنفَّذ داخل الحاوية عندما نشغلها. يمكنك تجريب أوامر أخرى يمكن للصورة أن تُنفذها، مثل: docker container run --rm ubuntu ls إذ يرتب الأمر ls كل الملفات في المجلد ضمن قائمة، بينما يُزيل الأمر rm-- الحاوية بعد التنفيذ، فلا تُحذف الحاويات تلقائيًا عادةً. لنتابع الآن مع أول حاوية "ubuntu" من خلال الملف "index.js" داخلها. لقد توقفت الحاوية عن العمل في اللحظة التي خرجنا منها. ولاستعراض جميع الحاويات، نستخدم الأمر container ls -a. إذ تستعرض الراية a- أو all-- كل الحاويات التي خرجنا منها توًا. $ docker container ls -a CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES b8548b9faec3 ubuntu "bash" 3 minutes ago Exited (0) 6 seconds ago hopeful_clarke أمامنا خياران يتعلقان بالتخاطب مع الحاوية: المعرّف في العمود الأول الذي يمكن استخدامه للتفاعل مع الحاوية في أي وقت، كما تقبل معظم الأوامر تمرير اسم الحاوية إليها. لاحظ أن الحاوية في مثالنا قد ولّدت تلقائيًا باسم "hopeful_clarke". تُظهر حالة الحاوية أننا خرجنا منها منذ 6 ثوان ويمكنك تشغيلها مجددًا باستخدام الأمر start الذي يقبل معرّف الحاوية id أو اسمها: $ docker start hopeful_clarke hopeful_clarke يشغّل الأمر الحاوية نفسها التي عملنا عليها سابقًا، لكن انتبه إلى أننا أغفلنا لسوء الحظ استخدام الراية interactive-- وبالتالي لن نتمكن من التفاعل معها. مع ذلك فالحاوية تعمل فعلًا، ويُظهر تنفيذ الأمر container ls -a ذلك، إلا أننا لا نستطيع التواصل معها: $ docker container ls -a CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES b8548b9faec3 ubuntu "bash" 7 minutes ago Up (0) 15 seconds ago hopeful_clarke يمكنك أيضًا تجاهل الراية a- في الأمر السابق لعرض الحاويات التي تعمل فقط: $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES 8f5abc55242a ubuntu "bash" 8 minutes ago Up 1 minutes hopeful_clarke لنُنهي عمل الحاوية باستخدام الأمر kill يليه اسم الحاوية أو معرّفها ثم نحاول ثانية: $ docker kill hopeful_clarke hopeful_clarke يُرسل الأمر الإشارة SIGKILL إلى العملية ليجبرها على التوقف، ونستطيع التأكد من حالتها بتنفيذ الأمر container ls -a: $ docker container ls -a CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES b8548b9faec3 ubuntu "bash" 26 minutes ago Exited 2 seconds ago hopeful_clarke لنشغل الحاوية الآن في وضع التفاعل: $ docker start -i hopeful_clarke root@b8548b9faec3:/# سنجري بعض التعديلات على الملف"index.js" بإضافة شيفرة جافا سكربت، لكن تنقصنا الأداة اللازمة لذلك. سيكون المحرر Nano خيارًا جيدًا حاليًا، لذلك سننزّله ونشغله. يمكنك الاطلاع على طريقة استخدام هذا المحرر بإجراء بحث بسيط ضمن أي محرك بحث. انتبه أنه لا حاجة لاستخدام التعليمة sudo فنحن فعليًا داخل المجلد الجذري: root@b8548b9faec3:/# apt-get update root@b8548b9faec3:/# apt-get -y install nano root@b8548b9faec3:/# nano /usr/src/app/index.js وهكذا يكون Nano جاهزًا للاستخدام. التمرينان 12.3 - 12.4 حاول أن تحل التمرينين التاليين: التمرين 12.3: 101 Ubuntu استخدم script لتسجيل ما تفعله واحفظ الملف بالاسم "script-answers/exercise12_3.txt" واستخدم المحرر النصي Nano لتعديل الملف "usr/src/app/index.js/" ضمن الحاوية بإضافة السطر التالي: console.log('Hello World') يمكنك الاطلاع على طريقة استخدام محرر Nano بإجراء بحث بسيط ضمن أي محرك بحث. التمرين 12.4: 102 Ubuntu استخدم script لتسجيل ما تفعله واحفظ الملف بالاسم "script-answers/exercise12_4.txt" ثم ثبّت Node طالما أنك ضمن الحاوية، ثم شغِّل الملف "index" باستخدام الأمر ‍‍node /usr/src/app/index.js. من الصعب أحيانًا إيجاد تعليمات واضحة لتثبيت Node لهذا إليك بعض الأوامر التي يمكنك نسخها ولصقها: curl -sL https://deb.nodesource.com/setup_16.x | bash apt install -y nodejs لا بد أيضًا من تثبيت curl ضمن الحاوية بطريقة مشابهة لتثبيت Nano، وتأكد بعد اكتمال التثبيت من قدرتك على تنفيذ شيفرتك داخل الحاوية. root@b8548b9faec3:/# node /usr/src/app/index.js Hello World أوامر دوكر أخرى بعد أن ثبتنا Node ضمن الحاوية يمكننا تنفيذ شيفرة جافاسكريبت JavaScript داخلها. لنحاول الآن إنشاء صورة جديدة من الحاوية بعد التعديلات التي أجريناها. للأمر الصيغة التالية: "commit" + اسم أو معرف الحاوية + اسم الصورة الجديدة. يمكنك استخدام الأمر container diff للتحقق من التغييرات بين الصورة الأصلية والجديدة. $ docker commit hopeful_clarke hello-node-world كما يمكنك عرض قائمة بالصور الموجودة باستخدام image Is على النحو التالي: $ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE hello-node-world latest eef776183732 9 minutes ago 252MB ubuntu latest 1318b700e415 2 weeks ago 72.8MB hello-world latest d1165f221234 5 months ago 13.3kB وستتمكن من تشغيل الصورة الجديدة على النحو التالي: docker run -it hello-node-world bash root@4d1b322e1aff:/# node /usr/src/app/index.js هناك عدة طرق لتحقيق النتيجة نفسها. لهذا دعونا نتحوّل إلى حل أفضل، وسنبدأ أولًا بإزالة الحاوية القديمة باستخدام الأمر container rm: $ docker container ls -a CONTAINER ID IMAGE COMMAND CREATED STATUS NAMES b8548b9faec3 ubuntu "bash" 31 minutes ago Exited (0) 9 seconds ago hopeful_clarke $ docker container rm hopeful_clarke hopeful_clarke أنشئ الملف "index.js" في المجلد الحالي واكتب التعليمة ('console.log('Hello, World ضمنها. لا حاجة للحاويات بعد، ولنتفادى أيضًا تثبيت Node. هناك العديد من الصور المفيدة الجاهزة للاستخدام يقدمها Docker Hub، لهذا سنستخدم الصورة " https://hub.docker.com/_/Node" التي تضم Node، وعلينا فقط اختيار الإصدار المناسب. كما تساعد الراية name-- في الأمر container run على تسمية الحاوية: $ docker container run -it --name hello-node node:16 bash سننشئ الآن مجلدًا للشيفرة ضمن الحاوية: root@77d1023af893:/# mkdir /usr/src/app وطالما أننا ضمن الحاوية، شغّل نسخة جديدة من الطرفية ونفّذ الأمر container cp لنسخ الملف من مكان وجوده في جهازك إلى الحاوية: $ docker container cp ./index.js hello-node:/usr/src/app/index.js يمكنك الآن تنفيذ الأمر ضمن الحاوية، كما يمكن تحضيرها على هيئة صورة جديدة، لكن هناك حل أفضل أيضًا. وهذا ما سنتابع البحث فيه لاحقًا. ترجمة -وبتصرف- للفصل Introduction to Containers من سلسلة Deep Dive Into Modern Web Development اقرأ أيضًا أبرز المفاهيم التي يجب عليك الإلمام بها عن الحاويات حاوية دوكر Docker ومخزن APCu في PHP كيفية تثبيت دوكر Docker على فيدورا لينكس
  22. سنعرض في هذا المقال طريقة التعامل مع اﻹعدادات اﻷساسية لنظام التشغيل لينكس أوبونتو، من خلال تطبيق "اﻹعدادات" الذي يسمح لك من خلال واجهة رسومية بسيطة وقوية، بضبط معظم ما تحتاجه بصفتك مستثمرًا للنظام، وذلك من إعدادات تتعلق بالعتاد الصلب والاتصال والتطبيقات والخصوصية والحماية واﻹنترنت وإعدادات اللغة وسهولة الوصول أو (اﻹتاحة)، وغيرها من اﻹعدادات. ننصحك قبل قراءة المقال أن تطلع على طريقة التعامل مع سطح مكتب أوبونتو وكذلك طريقة التعامل مع المجلدات والملفات. للوصول إلى هذا التطبيق، انقر على زر "أظهر التطبيقات" ضمن شريط التطبيقات لتظهر لك نافذة التطبيقات، ثم انقر على تطبيق "اﻹعدادات"؛ كما يمكنك الوصول إلى التطبيق مباشرةً من شريط المهام، أو بالنقر على سطح المكتب بالزر اﻷيمن لمؤشر الفأرة، واختيار "إعدادات settings". يعرض الشريط الجانبي للنافذة قوائم منفصلة من اﻹعدادات، تتعلق كل قائمة منها بمجموعة محددة، مثل الاتصالات والمظهر والتنسيقات المحلية واﻹقليمية، وغير ذلك، وسنناقش كل منها بشيء من التفصيل. ملاحظة: قد يتغير شريط المهام في نافذة التطبيق وفقًا للموضوع الذي نضبط إعداداته، إذ تظهر أحيانًا أشرطة مهام إضافية أو أزرار قوائم إضافية لتطبيق التغييرات التي نريدها بسرعة. إعدادات شبكات الاتصال يتيح لك أبونتو الاتصال مع معظم شبكات الاتصالات السلكية واللاسلكية، وحزم الاتصال العريضة عبر شبكات الهاتف المحمول وغيرها الكثير، لذلك سنطلع في الفقرات القادمة على طريقة إعداد وضبط بعض أنواعها. 1. إعداد واي فاي WiFi بالنقر على هذا الخيار، يبدأ معالج اﻹعدادات بالبحث عن الشبكات المرئية المتوفرة في الجوار ويضعها في قائمة على رأسها الشبكة اللاسلكية التي تتصل معها حاليًا. وفي حال لم تتصل بعد بأي شبكة لكنك تعرف معلومات الاستيثاق الخاصة بالولوج إليها (كأن تكون شبكتك الشخصية أو شبكة الشركة التي تعمل فيها)، انقر على هذه الشبكة، وستظهر لك نافذة تطلب إليك كتابة عبارة الوصول المشتركة: بعد كتابة كلمة السر أو عبارة الاستيثاق، انقر على الزر "اتصل"، وستصبح الشبكة جاهزةً للاستخدام؛ كما سيظهر إلى جوارها إشارة (صح)، باﻹضافة إلى أيقونة تشير إلى قوة اﻹشارة المستقبلة. قد يظهر إلى جوار الشبكة التي تتصل بها أيضًا أيقونة على شكل قفل لتدلّك أنّ هذه الشبكة محمية بآلية تشفير محددة. تفحص تفاصيل الاتصال لتفحص إعدادات الشبكة التي اتصلت بها، انقر على أيقونة اﻹعدادات في آخر السطر الذي يعرض اسم الشبكة. تعرض نافذة التفاصيل بعض النقاط العامة، مثل قوة اﻹشارة المستقبلة، وسرعة الاتصال، وعناوين بروتوكول الإنترنت IP، وعنوان بروتوكول وصول الوسائط (ماك MAC)، وذلك تحت مسمى "عنوان العتاد". وما يهمنا فعلًا في هذه النافذة هما الخياران: اتصل تلقائيًا: باختياره سيتصل الحاسوب بهذه الشبكة تلقائيًا بمجرد التقاط إشارتها في الجوار لتصبح الشبكة المبدئية. اجعله متاحًا للمستخدمين اﻵخرين: بتفعيل هذا الخيار يمكن لأي مستخدم مسجَّلٍ في نظامك أن يصل إلى هذه الشبكة، وإن لم يكن مفعلًا ستستخدمه أنت فقط. تشاهد أسفل النافذة زرًا باللون اﻷحمر عنوانه "انسَ الاتصال". انقر عليه لحذف الشبكة الحالية من قائمة الشبكات التي وجدها النظام في الجوار، وذلك إن أردت إعادة ضبط معلومات الاتصال من جديد. لن نتابع في تفصيل بقية النوافذ لأن مواضيعها خارج نطاق سلسلة المقالات هذه، لكن تجدر اﻹشارة إلى أنّ نافذة "أمان"، ستعرض لك كلمة السر التي أدخلتها عند تأسيس اتصالك مع الشبكة، ويمكنك العودة إليها دائمًا إن أردت تذكر الكلمة، وذلك بعد تفعيل خيار "أظهر كلمة السر". لتطبيق أية تغييرات أجريتها على تفاصيل شبكة الاتصال، انقر الزر "طبّق" أعلى يسار النافذة أو "ألغِ" للخروج دون أن تحفظ أية تغييرات. الاتصال بشبكة لاسلكية مخفية الشبكة اللاسلكية المخفية هي شبكة لا تبث هويتها SSID، ويُعَد ذلك عاملًا من عوامل اﻷمان. إن كنت تعرف بوجود شبكة لاسلكية خفية في الجوار وتريد الاتصال بها، فعليك أولًا أن تعرف هويتها تمامًا، وأن تعرف نظام التشفير الذي تستخدمه. وعند توفر هاتين المعلومتين، انقر على زر قائمة الاتصال اللاسلكي في شريط مهام نافذة اﻹعدادات (يبدو على شكل ثلاث نقاط متعامدة)، ثم اختر بعد ذلك اﻷمر "اتصل بشبكة مخفية" من القائمة المنسدلة، وستظهر لك الشاشة التالية: عليك كتابة اسم الشبكة في حقل "اسم الشبكة network name"، واختيار نظام اﻷمان من الحقل "أمان واي-فاي wifi security". انقر بعد ذلك على زر "اتصل" لتتمكن من استخدام الشبكة إن كان ما أدخلته صحيحًا. تفعيل نقطة بث شبكة لاسلكية إن أردت مشاركة اتصالك باﻹنترنت مع مجموعة من المستخدمين اﻵخرين عن طريق إنشاء شبكة لا سلكية خاصة بك، فاﻷمر بسيط جدًا، وذلك من خلال الخيار "شغل نقطة بث واي فاي" المتواجد ضمن قائمة الاتصال اللاسلكي التي أشرنا إليها سابقًا. اختر اسمًا لشبكتك ثم ضع كلمة السر التي ينبغي على المشتركين استخدامها لولوج الشبكة، ثم انقر على الأمر "شغّل". 2. إعداد الشبكة يعرض هذا اﻹعداد بقية أنواع شبكات الاتصال التي يدعمها حاسوبك، مثل الاتصال السلكي عبر محول الشبكة السلكية ethernet، أو اتصالات الحزمة العريضة عن طريق الشبكات الخلوية، أو الشبكات الافتراضية الخاصة VPN، وغيرها. تعرض لقطة الشاشة السابقة وجود محوّل شبكة سلكي تحت القسم "سلكي Wired" لكنه معطل لأن كابل الشبكة غير موصول بالمحوّل؛ بينما يشير القسم الثاني "حزمة خلوية عريضة Mobile Broadband" إلى وجود اتصال فعّال باﻹنترنت عن طريق الاتصال بالهاتف المحمول، ويعرض لك تفاصيل هذا الاتصال. يدل القسم الثالث "VPN" على وجود شبكة افتراضية اسمها "VPN1" لكنها معطلة؛ أما القسم اﻷخير، فهو مخصص لحالات وجود "وسيط الشبكة proxy"، وهو خادم وسيط بين حاسوبك وبقية الحواسيب على الشبكة؛ أو بين حاسوبك وشبكة اﻹنترنت. لا يوجد وسيط شبكة كما تدل اﻹعدادات، فهو معطل؛ لكن إن كان موجودًا، فانقر على زر اﻹعدادات في حقل "وسيط الشبكة"، ثم اختر اﻷمر "تلقائي" للكشف عن اﻹعدادات تلقائيًا أو "يدويًا" ﻹدخالها بنفسك. تظهر لك عند النقر على زر اﻹعدادات إلى جوار كل نوع من أنواع الاتصالات المعروضة نافذة تفاصيل الاتصال، وهي مشابهة من ناحية الخيارات لتفاصيل اتصال واي-فاي، ولن نخوض فيها أيضًا. 3. إعداد البلوتوث يتيح لك هذا الإعداد القدرة على البحث عن أية أجهزة تدعم اتصال بلوتوث إن كان حاسوبك يدعم هذا الخيار، وسيظهر حاسوبك عند اﻵخرين بنفس الاسم الذي اخترته أثناء تثبيت النظام. يبحث حاسوبك باستمرار عن اﻷجهزة الموجودة في النطاق ويعرضها في قائمة. للإتصال بأي جهاز مجاور موجود ضمن قائمة اﻷجهزة التي وجدها النظام، انقر على اسم الجهاز، وستظهر لك نافذة تحتوي على رمز التحقق الذي ينبغي على الجهاز المستهدف إدخاله لتأكيد قبول الاتصال مع حاسوبك. عندما يقبل الطرف اﻵخر الاقتران مع حاسوبك، سيكون كل شيء جاهزًا ﻹرسال الملفات واستقبالها. إرسال ملف من حاسوبك إلى جهاز مقترن به عبر بلوتوث عندما يقترن حاسوبك بجهاز ما، وتظهر عبارة "متصل" إلى جوار هذا الجهاز، فانقر عندها على هذا الجهاز، وستظهر لك النافذة التالية. انقر على الزر "إرسال ملفات"، ثم اختر الملف الذي تريد إرساله من النافذة التي ستظهر. ستبدأ بعدها مباشرةً عملية النقل وتدلك نافذة اﻹرسال على تقدم العملية. استقبال ملف من جهاز مقترن بحاسوبك عبر بلوتوث تتم عملية الاستقبال آليًا من الجهاز المقترن بالحاسوب، ويخبرك النظام بأنه استقبل ملفًا من هذا الجهاز وخزّنه في التنزيلات عندما تنتهي العملية. إعدادات مظهر سطح المكتب ومحتوياته يتيح لك سطح مكتب جنوم خيارات متعددة للتحكم بمظهر سطح المكتب وموقع شريط التطبيقات وسماته اللونية وحجم أيقوناته وغيرها. سنتعرف في هذه الفقرة على أهم اﻹعدادات التي يمكن التحكم بها من خلال تطبيق "إعدادات". اختيار خلفية سطح المكتب افتح تطبيق اﻹعدادات، ثم انتقل إلى الخيار "خلفية"، وستظهر لك الشاشة التالية: يمكنك اختيار أية صورة من الصور الموجودة أصلًا مع هذه النسخة من أوبونتو، وذلك بالنقر عليها لتتحول مباشرةً إلى خلفية لسطح المكتب؛ كما يمكن النقر على زر "أضف صورة" لاختيار صورة أخرى من اختيارك. اختيار سمة وضبط خصائص شريط التطبيقات يتيح لك أوبونتو مجموعة إعدادات لضبط المظهر العام لسطح المكتب والنوافذ من ناحية الشكل واللون، وهذا ما سنناقشه تاليًا. اختيار السمة اللونية انتقل إلى خيار "مظهر" في الشريط الجانبي لنافذة اﻹعدادات، وستظهر لك النافذة التالية: يتيح لك سطح مكتب جنوم إمكانية استخدام ثلاث سمات لسطح المكتب وهي: سمة اﻷلوان الفاتحة light: وتتميز النوافذ بخلفيتها البيضاء وشريط المهام الرمادي الفاتح. السمة القياسية standard: وتتميز النوافذ فيها بالخلفية البيضاء وشريط المهام الأسود. سمة اﻷلوان الداكنة dark: وتتميز النوافذ فيها بالخلفية الرمادية الداكنة وشريط المهام اﻷسود. بالنقر على أي من هذه السمات نقرةً واحدة، سيُطبّق النظام التغيرات مباشرةً. التحكم بإعدادات عرض شريط التطبيقات يتيح لك سطح مكتب جنوم الخيارات التالية: اﻹخفاء التلقائي Auto-hide the dock: بالنقر على الزالقة المجاورة لهذا الخيار، ستتحول إلى اللون البنفسجي ويختفي شريط التطبيقات عن سطح المكتب لكن بمجرد اﻹقتراب من مكانه يظهر مجددًا. التحكم بحجم اﻷيقونات Icon size: يمكنك جعل اﻷيقونات كبيرة بحجم 64 بكسل أو صغيرة بحجم 16 بكسل، أو أن تختار أي قيمة بينهما بما يناسبك عن طريق الزالقة المجاورة لهذا الخيار. وتجدر اﻹشارة إلى أنّ القيمة الافتراضية لحجم اﻷيقونات هي 48 بكسل. شاشة عرض الشريط show on screen: يُمكِّنك هذا الخيار من إظهار الشريط على الشاشة الحالية فقط، أو إظهاره على كل الشاشات المتصلة بحاسوبك، أو ما يُعرف باسم توسعة سطح المكتب. تغيير موقع شريط التطبيقات position on screen: يقع شريط التطبيقات في النسخة العربية عموديًا على يمين الشاشة، لكن إن أردته أن يكون إلى اﻷسفل كما هي حال أشرطة المهام في ويندوز أو ماك، فاختر القيمة "أسفل button"، أو اختر القيمة "يسار left" إن أردته أن يظهر يسارًا. ضبط التنبيهات التنبيهات هي مجموعة من الرسائل النصية أو الصوتية التي يصدرها النظام أو أحد التطبيقات المثبتة ﻹبلاغ المستخدم بوجود مشكلة أو اﻹشارة إلى تنفيذ عمل ما. وتُعَدّ التنبيهات إحدى موارد النظام التي يمكن للبرمجيات الوصول إليها، فعندما يكتمل تحميل برنامج مثلًا، سيعرض لك النظام تنبيهًا على شكل رسالة منبثقة أعلى ومنتصف شريط المهام يخبرك فيها باكتمال التحميل، أو عندما يتصل النظام بشبكة أو ينقطع الاتصال تعرض لك رسائل وتنبيهات صوتية مناسبة. ولضبط التنبيهات التي تريدها أن تظهر أو إهمال تنبيهات لا تعتقد أنها مهمة، فافتح تطبيق "إعدادات"، وانتقل إلى الخيار "التنبيهات": تتيح لك هذه النافذة مجموعتين من الخيارات، اﻷولى تتعلق بالنظام واﻷخرى بالتطبيقات: تنبيهات النظام يمكن التحكم بميزتين تتعلقان بتنبيهات النظام وهما: عدم اﻹزعاج Do not disturb: عند تفعيل هذا الخيار بالنقر على الزالقة المجاورة، سيمنع النظام ظهور أية تنبيهات أيًا كان مصدرها، ويعرض أيقونة عدم اﻹزعاج إلى جوار الساعة وسط شريط المهام. تنبيهات شاشة القفل: سيعرض النظام عند تفعيل هذا الخيار التنبيهات حتى لو كانت شاشة سطح المكتب مقفلة؛ بينما لن يعرض التنبيهات عند تقفل الشاشة في حال لم يكن هذا الخيار مفعلًا (الزالقة إلى اليسار ورمادية اللون). تنبيهات التطبيقات يعرض هذا القسم من النافذة جميع التطبيقات التي تستخدم تنبيهات النظام لعرض رسائل حالة التطبيق. وبالنقر على أيٍ من هذه التطبيقات، ستنبثق نافذة تضم مجموعة من الخيارات هي: التنبيهات: لعرض تنبيهات التطبيق عمومًا أو إبطالها. التنبيهات الصوتية: تفعيل أو إسكات التنبيهات الصوتية. نوافذ التنبيهات المنبثقة: يعرض النظام تنبيهات التطبيق على شكل نوافذ منبثقة عند تفعيل الخيار، ولن يعرضها ضمن قائمة منبثقة، بل ضمن قائمة التنبيهات فقط (تظهر القائمة بالنقر على الساعة في شريط المهام) إن لم يكن مفعلًا. أظهر محتوى الرسائل في نافذة منبثقة: يعرض النظام رسائل التطبيق عند تفعيل الخيار في نوافذ منبثقة مستقلة بدلًا من عرضها في شريط المهام أو في قائمة التنبيهات. تنبيهات شاشة القفل: يعرض تنبيهات التطبيق في شريط المهام أو في قائمة التنبيهات حتى لو كانت الشاشة مقفلة. أظهر محتوى الرسالة في شاشة القفل: عند تمكين هذا الخيار، سيعرض النظام رسالة التطبيق على الشاشة حتى لو كانت مقفلة. البحث في سطح المكتب والتطبيقات سيعرض لك النظام عند النقر على خيار "البحث" ضمن الشريط الجانبي لتطبيق "اﻹعدادات" قائمةً باﻷماكن التي سيبحث فيها النظام عن مدخلاتك عندما تحاول البحث عن شيء ما. تُرتب هذه اﻷماكن وفقًا لأولوية ظهور نتائج البحث المتعلقة بها، فالمكان الموجود أعلى القائمة، ستظهر نتائجه أولًا وهكذا. يمكنك أيضًا تغيير اﻷولوية بالنقر على زر النقاط الثلاث المتعامدة آخر كل حقل واختيار "انقل لأعلى" أو "انقل لأسفل" لرفع ترتيب الحقل أو تخفيضه؛ كما يمكنك النقر على الزالقة المجاورة لكل حقل ﻹلغاء البحث في هذا المكان، أو يمكنك النقر أيضًا على الزالقة الموجودة في شريط مهام النافذة لتعطيل البحث في هذه اﻷماكن. اقرأ أيضًا تعرّف على سطح مكتب أوبونتو 20.04 التعامل مع المجلدات والملفات في أوبونتو 20.04 تغيير اللغة في نظام لينكس أوبنتو إلى العربية
  23. يتألف موقع ويب من ملفات عدة منها ملفات المحتوى وملفات الشيفرة وملفات التنسيق والوسائط المتعددة وغيرها، فعندما تبني موقعك، عليك تجميع هذه الملفات ضمن هيكلية معقولة على حاسوبك، والتأكد من أنها قادرة على التواصل مع بعضها وأنّ كل شيء يبدو على ما يرام قبل أن ترفع هذه الملفات إلى الخادم، إذ سيناقش هذا المقال بعض الأمور التي ينبغي الانتباه إليها لكي تنظم ملفاتك ضمن هيكلية واضحة لتكوين موقعك. مكان تجميع موقع ويب على حاسوبك عندما تشرع في بناء موقع ويب على حاسوبك، لا بد أن تُبقي كل الملفات المرتبطة به ضمن مجلد واحد يحاكي في تنظيمه تنظيم الملفات ضمن موقع الويب عندما تنشره، كما يمكنك اختيار أيّ مكان ضمن حاسوبك لوضع هذا المجلد شرط أن يكون إيجاده سهلًا مثل سطح المكتب أو في المجلد الرئيسي Home أو ضمن المجلد الجذري للقرص الصلب. اختر مكانًا لتخزِّن ضمنه مشروع موقع الويب، ثم انشئ مجلدًا جديدًا في هذا المكان وسمِّه web-projects أو ما شابه، إذ سيكون هذا المجلد المكان الذي تخزّن فيه جميع مواقع الويب التي تصممها. انشئ ضمن هذا المجلد مجلدًا جديدًا لتخزين موقعك الأول، وسمِّه test-site أو ما شابه. تسمية الملفات والمجلدات ستلاحظ خلال هذا المقال أننا نسمي المجلدات والملفات بأحرف صغيرة ودون فراغات وذلك لأن: تُعَدّ الكثير من الحواسب وخاصةً الخوادم حساسةً لحالة الأحرف، فإذا وضعت مثلًا صورةً على موقعك عنوانها test-site/MyImage.jpg وأردت تشغيلها من ملف آخر وكتبت العنوان test-site/myimage.jpg، فربما لا تعمل. لا تعامِل خوادم ويب والمتصفحات ولغات البرمجة الفراغات بالطريقة نفسها، فإذا وضعت فراغات في اسم الملف مثلًا، فستعامِل بعض الأنظمة اسم الملف هذا على أساس ملفين منفصلين، إذ تملأ بعض خوادم ويب الفراغات في أسماء الملفات بالرمز "%20" (وهو رمز المسافة الفارغة في عناوين URL)، وتكون النتيجة أخطاءً في جميع الروابط، ومن الأفضل أيضًا فصل الكلمات بشرطة hyphen مثل my-file.html بدلًا من الشرطة السفلية مثل my_file.html، والسبب في ذلك أنّ محرك بحث جوجل يعامل الشرطات على أساس فواصل بين الكلمات بينما لا يعامل الشرطات السفلية بالطريقة ذاتها. خلاصة الأمر أنه عليك اعتياد استخدام الأحرف الصغيرة دون فراغات، واستخدام الشرطات للفصل بين الكلمات حتى تدرك ما تفعل على الأقل، فإنّ تقيدك بذلك يريحك من بعض المشاكل التي قد تنبثق أمامك هنا وهناك. الهيكلية التي ينبغي أن يبنى عليها موقع ويب لنلق نظرةً فيما سيأتي على هيكلية الموقع البسيط الذي نبنيه، إذ يُعَدّ الشيء المشترك بين معظم مشاريع مواقع ويب هو إنشاء ملف HTML يعمل تلقائيًا عند استدعاء الموقع ويُدعى عادة "index"، بالإضافة إلى مجلد يحتوي على الصور وملفات التنسيق وملفات الشيفرة، فلنبن هذه الأشياء إذًا: index.html: استخدم محرر النصوص الذي تملكه لإنشاء ملف جديد يُدعى index.html، ثم احفظه ضمن المجلد test-site، إذ يحتوي هذا الملف عمومًا على محتويات الصفحة الرئيسية للموقع مثل النصوص والصور التي يراها الزائر عند دخول موقعك. المجلد images: أنشئ مجلدًا بهذا الاسم داخل المجلد test-site، إذ يضم كل الصور التي تستخدمها في موقعك. المجلد styles: أنشئ مجلدًا بهذا الاسم ثم احفظه ضمن المجلد test-site، إذ يضم كل الملفات التي تحتوي على شيفرة التنسيقات المورثة CSS والتي تتحكم بمظهر الصفحة مثل لون النصوص والخلفية. المجلد scripts: أنشئ مجلدًا بهذا الاسم ثم احفظه في المجلد test-site، إذ يضم شيفرة جافاسكربت التي تُستخدَم لإضافة وظائف تفاعلية إلى صفحتك مثل الأزرار التي تعرض بيانات عند نقرها. ملاحظة: قد تجد صعوبةً في رؤية أسماء الملفات كاملة على الحواسب التي تشغل النظام ويندوز لأنه يقدِّم خيارًا بإخفاء امتدادات الملفات معروفة النوع، وهذا الخيار مفعَّل افتراضيًا، إذ يمكنك عادةً إيقاف هذا الخيار بالانتقال إلى مستكشف ويندوز Windows Explorer ثم خيارات المجلد Folder Options وبعدها ألغ تفعيل خيار "إخفاء الامتدادات للملفات معروفة النوع Hide extensions for known file types"، كما يمكنك دومًا البحث عبر الويب لإيجاد التفاصيل الخاصة بنسختك من ويندوز. مسارات الملفات لا بد من تحديد مسار الملف بطريقة صحيحة حتى تتمكن الملفات في الموقع من التخاطب مع بعضها بعضًا، ومبدئيًا لا بد من تحديد وجهة ليتمكن أيّ ملف من معرفة مكان الآخر، ولتوضيح الأمر سنكتب بعض الشيفرة في الملف index.html لكي يعرض الصورة التي اخترتها لموقعك البسيط أو أية صورة مناسبة قد تجدها على حاسوبك أو على الويب: انسخ الصورة التي اخترتها إلى المجلد images. افتح الملف index.html وانقل الشيفرة التالية إليه كما هي تمامًا، ولا تكترث حاليًا بمعنى هذه الشيفرة، إذ سنهتم بالتفاصيل لاحقًا: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>My test page</title> </head> <body> <img src="" alt="My test image"> </body> </html> يُدرِج سطر شيفرة HTML التالي الصورة ضمن صفحتك: <img src="" alt="My test image"> لابد من إخبار HTML بمكان وجود الصورة، إذ تتواجد الصورة ضمن مجلد الصور وهذا المجلد موجود في المجلد نفسه الذي يحوي الملف index.html، وبالتالي سنحتاج إلى المسار images/your-image-filename للوصول من هذا الملف إلى الصورة، فإذا كان اسم الصورة firefox-icon.png، لكان المسار images/firefox-icon.png. ضع اسم المسار في شيفرة إدراج الصورة بين علامتي إقتباس بالشكل ""=src، ثم احفظ الملف ثم افتحه باستخدام المتصفح بالنقر المزدوج على أيقونة الملف، إذ ستظهر الآن الصورة المطلوبة. إليك بعض القواعد العامة في تحديد مسارات الملفات: لاستدعاء ملف يقع في المجلد نفسه الذي يقع فيه الملف الذي يستدعي، استخدم فقط اسم هذا الملف مثل my-image.jpg. للإشارة إلى ملف في مجلد فرعي مجاور للملف الذي يستدعي، اكتب اسم المجلد الفرعي متبوعًا بالمحرف "/" ثم اسم الملف المطلوب مثل subdirectory/my-image.jpg. للإشارة إلى ملف يقع في المجلد الأب للمجلد الذي يقع فيه الملف الذي يستدعيه، اكتب نقطتين .. ثم المحرف "/" ثم اسم الملف مثل my-image.jpg/... يمكنك الدمج بين القواعد السابقة كما تشاء مثل ‎../subdirectory/another-subdirectory/my-image.jpg، وهذا كل شيء تحتاجه إلى الآن. ملاحظة: يستخدِم نظام التشغيل ويندوز الشرطة الخلفية \ وليست الأمامية / لتحديد المسارات مثل C:\Windows، ولكن عليك استخدام الشرطة الأمامية دائمًا عند تطوير مواقع ويب حتى لو عملت على ويندوز. ما الذي يجب فعله أيضا لا شيء الآن، إذ يجب أن تبدو هيكلية موقعك مشابهةً للهيكلية التي تعرضها الصورة التالية: ترجمة -وبتصرف- للمقال Dealing with files اقرأ أيضًا التعامل مع الملفات في البرمجة رفع ملفات موقع الويب إلى خادم على الإنترنت الأدوات المستخدمة في بناء مواقع ويب
  24. لقد حان الوقت لتوسيع مشروعنا بعد أن أسسنا له قاعدةً معرفيةً جيدة، وسنسخّر لهذه المهمة كل إمكانيات React Native التي تعلمناها حتى الآن. سنغطي بالإضافة إلى توسيع المشروع نواحٍ جديدة، مثل الاختبارات والموارد الإضافية. اختبار تطبيقات React Native سنحتاج إلى إطار عمل للاختبارات عند البدء باختبار أي نوع من الشيفرة، لتنفيذ مجموعة من الحالات المختبرة، والتدقيق في نتائجها. فلاختبار تطبيق جافا سكربت JavaScript سنجد أن إطار العمل Jest هو الأكثر شعبية لأداء المهمة. ولاختبار تطبيقات React Native المبنية على المنصة Expo باستخدام Jest، ستزودنا Expo بمجموعة من إعدادات تهيئة على هيئة مجموعة جاهزة تدعى jest-expo. ولكي نستخدم المدقق ESLint في ملف اختبار Jest، سنحتاج إلى الإضافة eslint-plugin-jest الخاصة به. لنبدأ إذًا بتثبيت الحزم: npm install --save-dev jest jest-expo eslint-plugin-jest لاستخدام المجموعة jest-expo في Jest، لا بدّ من إضافة إعدادات التهيئة التالية في الملف "package.json"، مع سكربت الاختبار: { // ... "scripts": { // other scripts... "test": "jest" }, "jest": { "preset": "jest-expo", "transform": { "^.+\\.jsx?$": "babel-jest" }, "transformIgnorePatterns": [ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-router-native)" ] }, // ... } يطلب الخيار transform من Jest تحويل شيفرة الملفين "js." و"jsx." باستخدام مصرّف Babel. بينما يُستخدم الخيار transformIgnorePatterns لتجاهل مجلدات محددة ضمن المجلد "node_modules" أثناء تحويل الملفات. وتتطابق تقريبًا إعدادات Jest هذه مع تلك المقترحة في توثيق Expo. لاستخدام الإضافة eslint-plugin-jest، لا بدّ من وضعها ضمن مصفوفة الإضافات والموسِّعات في الملف "eslintrc.". { "plugins": ["react", "react-native"], "settings": { "react": { "version": "detect" } }, "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:jest/recommended"], "parser": "@babel/eslint-parser", "env": { "react-native/react-native": true }, "rules": { "react/prop-types": "off", "react/react-in-jsx-scope": "off" } } وللتأكد من نجاح التهيئة، أنشئ المجلد "tests" في المجلد "src"، ثم أنشئ ضمنه الملف "example.js" وضع فيه الشيفرة التالية: describe('Example', () => { it('works', () => { expect(1).toBe(1); }); }); لننفذ الآن هذا المثال من خلال الأمر: npm test. ينبغي أن يشير خرج العملية إلى نجاح الاختبار المتواجد في الملف "src/tests/example.js". تنظيم الاختبارات تقتضي إحدى طرق تنظيم ملفات الاختبارات بوضعها في مجلد وحيد يُدعى "tests". ويفضّل عند استخدام هذه الطريقة وضع ملفات الاختبار ضمن المجلدات الفرعية الملائمة كما نفعل مع ملفات الشيفرة، إذ ستُوضع الاختبارات التي تتعلق بالمكوّنات مثلًا في المجلّد "components"، وتلك التي تتعلق بالخدمات ستُوضع في المجلّد "utils"، وهكذا. سينتج عن هذا التنظيم الهيكلية التالية: src/ __tests__/ components/ AppBar.js RepositoryList.js ... utils/ authStorage.js ... ... أما الطريقة الأخرى فهي وضع ملف الاختبار بجانب ملف الشيفرة الذي سيستخدمه. ويعني ذلك أننا سنضع الملف الذي يتضمن اختبارات للمكوّن AppBar مثلًا في نفس المجلد الذي يحتوي شيفرة هذا المكوّن. وستنتج عن هذه الطريقة في التنظيم الهيكلية التالية: src/ components/ AppBar/ AppBar.test.jsx index.jsx ... ... شيفرة المكوّن في المثال السابق موجودةٌ في الملف "index.jsx"، بينما ستجد الاختبارات في الملف "AppBar.test.jsx". ولكي تجد بسهولة الملفات التي تحتوي الاختبارات، عليك أن تضعها في المجلد "tests" أو في ملفات تحمل إحدى اللاحقتين "test." أو "spec."، أو أن تهيئ يدويًا الأنماط العامة global patterns. اختبار المكونات بعد أن ضبطنا إعدادات Jest وجربناها بتنفيذ مثال بسيط، حان الوقت لنتعرف على كيفية اختبار المكوّنات. يتطلب اختبار المكوّنات، كما نعلم، طريقة لتفسير الخرج الناتج عن تصيير المكوّن ومحاكاة عملية معالجة الأحداث المختلفة مثل الضغط على زر. ولحسن الحظ تتوافر عائلة من مكتبات الاختبار تؤمن مكتبات لاختبار مكوًنات واجهة المكوّن في منصات عدة. وتتشارك جميع هذه المكتبات بواجهة برمجية واحدة API لاختبار مكونات واجهة المستخدم بأسلوب يركّز على المستخدم. تعرّفنا في القسم 5 على إحدى هذه المكتبات وهي React Testing Library. لكن لسوء الحظ فهذه المكتبة مخصصة فقط لاختبار تطبيقات الويب المبنية باستخدام React. لكن بالطبع هناك مقابل لها في React Native وهي المكتبة React Native Testing Library التي سنستخدمها في اختبار مكوّنات تطبيقات React Native. وكما ذكرنا فإن جميع هذه المكتبات تتشارك بنفس الواجهة البرمجية، ولن تضطر إلى تعلّم الكثير من المفاهيم الجديدة. بالإضافة إلى هذه المكتبة، سنحتاج إلى مجموعة من مُطابقات Jest مخصصة للمكتبة مثل toHaveTextContent و toHaveProp. تزوّدنا المكتبة jest-native بهذه المُطابقات، لذلك لا بدّ أولًا من تثبيت هذه الحزم: npm install --save-dev react-test-renderer@17.0.1 @testing-library/react-native @testing-library/jest-native ملاحظة: إذا واجهت مشاكل في اعتمادية النظير peer، فتأكد من مطابقة إصدار react-test-renderer مع نسخة react للمشروع في أمر npm install المذكور أعلاه. يمكنك التحقق من إصدار react من خلال تنفيذ الأمر: npm list react --depth=0 وفي حال فشل التثبيت بسبب مشكلات اعتمادية النظير، فحاول مرةً أخرى باستخدام الراية ‎--legacy-peer-deps في الأمر npm install. لنتمكن من استخدام هذه المُطابقات، علينا توسيع الكائن expect العائد للمكتبة Jest. ويجري تنفيذ ذلك باستخدام ملف إعداد عام. لذلك أنشئ الملف "setupTests.js" في المجلد الجذري لمشروعك، وهو نفسه المجلد الذ++ي يحتوي الملف "package.json". أضف الشيفرة التالية إلى هذا الملف: import '@testing-library/jest-native/extend-expect'; هيئ الملف السابق على انه ملف إعداد ضمن أوامر تهيئة Jest الموجودة في الملف "package.json": { // ... "jest": { "preset": "jest-expo", "transform": { "^.+\\.jsx?$": "babel-jest" }, "transformIgnorePatterns": [ "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*|react-router-native)" ], "setupFilesAfterEnv": ["<rootDir>/setupTests.js"] } // ... } تعتمد المكتبة على مفهومين أساسيين هما الاستعلامات (query) وإطلاق الأحداث (fire event)؛ إذ تُستخدم الاستعلامات لاستخلاص مجموعة من العقد من المكوّن الذي يُصيّر بواسطة الدالة render، ونستفيد منها في اختبار وجود عنصر أو كائن ما، مثل نص معين، ضمن المكوّن المُصيَّر. يمكنك تحديد العقد باستخدام الخاصية testID ثم الاستعلام عنها باستخدام الدالة getByTestId. تقبل جميع المكوّنات البنيوية في الخاصية testID. وإليك مثالًا عن كيفية استخدام الاستعلامات: import { Text, View } from 'react-native'; import { render } from '@testing-library/react-native'; const Greeting = ({ name }) => { return ( <View> <Text>Hello {name}!</Text> </View> ); }; describe('Greeting', () => { it('renders a greeting message based on the name prop', () => { const { debug, getByText } = render(<Greeting name="Kalle" />); debug(); expect(getByText('Hello Kalle!')).toBeDefined(); }); }); تعيد الدالة render الاستعلامات ودوال مساعدة أخرى مثل الدالة debug التي تطبع شجرة React بطريقة واضحة بالنسبة للمستخدم. استخدم هذه الدالة إن كنت غير متأكد من شكل المكوّن الذي صيَّرته الدالة render. يمكننا أن نحصل على العقدة Text التي تحتوي نصًا محددًا عن طريق الدالة getByText. وللاطلاع على كل الاستعلامات المتاحة، راجع توثيق المكتبة React Native Testing. يُستخدم المُطابق toHaveTextContent للتأكد من أن المحتوى النصي للعقدة صحيح. وللاطلاع كذلك على كل المُطابقات الخاصة بالمكتبة React Native، راجع توثيق jest-native. وتذكر أنك ستجد معلومات عن كل مُطابقٍ عام في توثيق Jest. أما المفهوم الثاني الذي ترتكز عليه المكتبة React Native Testing في عملها هو إطلاق الأحداث firing events، إذ يمكننا إطلاق حدث ضمن عقدة معينة باستخدام توابع الكائن fireEvent. سنستفيد من ذلك على سبيل المثال في طباعة نص ضمن حقل نصي أو على زر. وإليك مثال عن اختبار تسليم نموذج بسيط: import { useState } from 'react'; import { Text, TextInput, Pressable, View } from 'react-native'; import { render, fireEvent } from '@testing-library/react-native'; const Form = ({ onSubmit }) => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const handleSubmit = () => { onSubmit({ username, password }); }; return ( <View> <View> <TextInput value={username} onChangeText={(text) => setUsername(text)} placeholder="Username" /> </View> <View> <TextInput value={password} onChangeText={(text) => setPassword(text)} placeholder="Password" /> </View> <View> <Pressable onPress={handleSubmit}> <Text>Submit</Text> </Pressable> </View> </View> ); }; describe('Form', () => { it('calls function provided by onSubmit prop after pressing the submit button', () => { const onSubmit = jest.fn(); const { getByPlaceholderText, getByText } = render(<Form onSubmit={onSubmit} />); fireEvent.changeText(getByPlaceholderText('Username'), 'kalle'); fireEvent.changeText(getByPlaceholderText('Password'), 'password'); fireEvent.press(getByText('Submit')); expect(onSubmit).toHaveBeenCalledTimes(1); // تحوي أول وسيط من أول استدعاء onSubmit.mock.calls[0][0] expect(onSubmit.mock.calls[0][0]).toEqual({ username: 'kalle', password: 'password', }); }); }); نريد في هذا الاختبار أن نتأكد من أنّ الدالة onSubmit ستُستدعى على نحوٍ صحيح بعد أن تُملأ حقول النموذج باستخدام التابع fireEvent.changeText ويُضغط على الزر باستخدام التابع fireEvent.press. ولتحري إذا ما استُدعيت الدالة onSubmit وبأية وسطاء، يمكن استخدام الدالة المقلّدة mock function؛ والدوال المقلدة هي دوال ذات سلوك مبرمج مسبقًا كأن تعيد قيمةً محددة. كما يمكننا تحديد توقّعات expectations لهذه الدوال، فمثلًا "توقّع أن الدالة المقلّدة قد استدعيت مرةً واحدة". يمكنك الاطلاع على قائمة بكل التوقعات المتاحة في توثيق توقعات Jest. عدّل وجرّب في تلك الأمثلة السابقة، وأضف اختبارات جديدة في المجلد "tests "، بحيث تتأكد من فهمك للأفكار السابقة قبل الغوص أعمق في موضوع الاختبارات. التعامل مع الاعتماديات أثناء الاختبارات يُعد اختبار المكوّنات سهلًا نوعًا ما لأنها صرفة بشكلٍ أو بآخر. فلا تعتمد المكوّنات الصرّفة على التأثيرات الجانبية side effects، مثل طلبات الشبكة أو استخدام واجهات برمجية أصيلة، مثل AsyncStorage. فالمكوّن Form ليس صرفًا بقدر المكون Greeting، لأن تغيرات حالته قد تُعد أثرًا جانبيًا. لكن الاختبارات تبقى سهلةً مع ذلك. لنلقِ الآن نظرةً على استراتيجية لاختبار المكوّنات مع الآثار الجانبية، وسنختار على سبيل المثال المكوّن RepositoryList من تطبيقنا. لهذا المكوّن حاليًا أثر جانبي واحد وهو استعلام GraphQl الذي يحضر قائمة بالمستودعات المقيّمة. يُستخدم المكوّن حاليًا كالتالي: const RepositoryList = () => { const { repositories } = useRepositories(); const repositoryNodes = repositories ? repositories.edges.map((edge) => edge.node) : []; return ( <FlatList data={repositoryNodes} // ... /> ); }; export default RepositoryList; ينتج التأثير الجانبي عن استخدام الخطاف useRepositories الذي يرسل استعلام GraphQL. هناك عدة طرق لاختبار المكوّن، إحدى هذه الطرق هي تقليد استجابات المكتبة Apollo Client كما هو مشروح في توثيقها. أما الطريقة الأبسط هي افتراض أن المكوّن يعمل كما نتوقع (ويُفضل من خلال اختباره)، ثم استخلاص الشيفرة "الصرفة" له ووضعها في مكوّن آخر مثل RepositoryListContainer: export const RepositoryListContainer = ({ repositories }) => { const repositoryNodes = repositories ? repositories.edges.map((edge) => edge.node) : []; return ( <FlatList data={repositoryNodes} // ... /> ); }; const RepositoryList = () => { const { repositories } = useRepositories(); return <RepositoryListContainer repositories={repositories} />; }; export default RepositoryList; يحتوي المكوّن الآن التأثيرات الجانبية فقط وتنفيذه سهل. يمكن اختبار المكوّن RepositoryListContainer بتمرير بيانات عن مستودع من القائمة من خلال الخاصية repositories، والتأكد أنّ معلومات المحتوى المُصيَّر صحيحة. التمرينان 10.17 - 10.18 10.17 اختبار قائمة المستودعات المقيمة أنجز اختبارًا يتأكد أن المكوّن RepositoryListContainer سيصيِّر بصورةٍ صحيحة كلًا من: اسم المستودع ووصفه واللغة وعدد التشعبات وعدد النجوم ومعدل التقييم وعدد التقييمات. تذكر أنه بالإمكان الاستفادة من المطابق toHaveTextContent في التحقق من احتواء العقدة على سلسلة نصية محددة. const RepositoryItem = (/* ... */) => { // ... return ( <View testID="repositoryItem" {/* ... */}> {/* ... */} </View> ) }; كما يمكنك استخدام الاستعلام getAllByTestId للحصول على كل العقد التي تمتلك قيمة محددة للخاصية testID على هيئة مصفوفة، وإن كنت غير متأكد من المكوّن الذي صُيَّر، استفد من الدالة debug في تفكيك نتيجة التصيير. const repositoryItems = getAllByTestId('repositoryItem'); const [firstRepositoryItem, secondRepositoryItem] = repositoryItems; // توقع شيئًا من عنصر المستودع الأول والثاني استخدم الشيفرة التالية أساسًا لاختبارك: describe('RepositoryList', () => { describe('RepositoryListContainer', () => { it('renders repository information correctly', () => { const repositories = { pageInfo: { totalCount: 8, hasNextPage: true, endCursor: 'WyJhc3luYy1saWJyYXJ5LnJlYWN0LWFzeW5jIiwxNTg4NjU2NzUwMDc2XQ==', startCursor: 'WyJqYXJlZHBhbG1lci5mb3JtaWsiLDE1ODg2NjAzNTAwNzZd', }, edges: [ { node: { id: 'jaredpalmer.formik', fullName: 'jaredpalmer/formik', description: 'Build forms in React, without the tears', language: 'TypeScript', forksCount: 1619, stargazersCount: 21856, ratingAverage: 88, reviewCount: 3, ownerAvatarUrl: 'https://avatars2.githubusercontent.com/u/4060187?v=4', }, cursor: 'WyJqYXJlZHBhbG1lci5mb3JtaWsiLDE1ODg2NjAzNTAwNzZd', }, { node: { id: 'async-library.react-async', fullName: 'async-library/react-async', description: 'Flexible promise-based React data loader', language: 'JavaScript', forksCount: 69, stargazersCount: 1760, ratingAverage: 72, reviewCount: 3, ownerAvatarUrl: 'https://avatars1.githubusercontent.com/u/54310907?v=4', }, cursor: 'WyJhc3luYy1saWJyYXJ5LnJlYWN0LWFzeW5jIiwxNTg4NjU2NzUwMDc2XQ==', }, ], }; // أضف شيفرة اختبارك هنا }); }); }); يمكن أن تضع ملف الاختبار أينما تشاء، لكن من الأفضل أن تتبع إحدى الطرق التي ذكرناها في تنظيم ملفات الاختبار. استخدم المتغير repositories مثل مصدر للبيانات في الاختبار، ولا حاجة لتغيير قيمته. تحتوي البيانات على مستودعين، فعليك بالتالي التحقق من وجود المعلومات في كليهما. 10.18 اختبار نموذج تسجيل الدخول أنجز اختبارًا يتحقق أن الدالة onSubmit ستُستدعى بوسطاء مناسبين عندما يُملأ حقلي اسم المستخدم وكلمة المرور في النموذج ويُضغط على زر الإرسال. ينبغي أن يكون الوسيط الأول كائنًا يمثّل قيم النموذج، ويمكنك إهمال بقية وسطاء الدالة. تذكر أنك قد تستعمل توابع fireEvent لتنفيذ أحداث ودوال مقلِّدة للتحقق من استدعاء معالج الحدث onSubmit أو لا. لا حاجة لاختبار أي شيفرة متعلقة بالمكتبة Apollo Client أو AsyncStorage والموجودة في الخطاف useSignIn. وكما فعلنا في التمرين السابق، استخلص الشيفرة الصرفة وضعها في مكوّن خاص واختبره. انتبه إلى إرسالات نماذج Formik فهي غير متزامنة، فلا تتوقع استدعاء الدالة onSubmit مباشرة بعد الضغط على زر الإرسال. يمكنك الالتفاف على المشكلة بجعل دالة الاختبار دالة غير متزامنة باستخدام التعليمة Async واستخدام الدالة المساعدة waitFor العائدة للمكتبة React Native Testing. كما يمكنك يمكن أن تستخدم الدالة waitFor لانتظار تحقق التوقعات. فإن لم يتحقق التوقع خلال فترة محددة، سترمي الدالة خطأً. إليك مثالًا يشير إلى أسلوب استخدام هذه الدالة: import { render, fireEvent, waitFor } from '@testing-library/react-native'; // ... describe('SignIn', () => { describe('SignInContainer', () => { it('calls onSubmit function with correct arguments when a valid form is submitted', async () => { //صيّر المكوّن ثم املأ الحقول النصية ثم اضغط زر الإرسال await waitFor(() => { // توقع أن تُستدعى دالة الإرسال مرة واحد وأن يكون الوسيط الأول صحيحًا }); }); }); }); توسيع التطبيق سنبدأ باستخدام كل ما تعلمناه في توسيع تطبيقنا، فلا يزال التطبيق يفتقر لبعض النواحي، مثل عرض معلومات مستودع وتسجيل مستخدمين جدد. سنركز في التمارين القادمة على هذه النقاط. التمرينات 10.19 - 10.24 10.19: واجهة لعرض مستودع واحد أنجز واجهةً لعرض معلومات عن مستودع واحد، تتضمن نفس المعلومات التي تُعرض في قائمة المستودعات بالإضافة إلى زر لفتح المستودع في غيت هب Github. من الجيد أن تجد طريقةً لإعادة استخدام المكونين RepositoryItem و RepositoryList وأن تعرض الزر الذي أشرنا إليه بناء على خاصية محددة تعيد قيمة منطقية مثلًا. ستجد عنوان المستودع ضمن الحقلurl العائد للنوع "Repository" في تخطيط GraphQL. تستطيع إحضار مستودع واحد من خادم Apollo بإجراء الاستعلام repository. يُمرَّر إلى هذا الاستعلام وسيط واحد وهو المعرّف المميز id للمستودع. إليك مثالًا عن استخدام الاستعلام repository. { repository(id: "jaredpalmer.formik") { id fullName url } } اختبر استعلامك كما هو معتاد في أرضية عمل Apollo قبل استخدامه في التطبيق. إن لم تكن على دراية بتخطيط GrapQL أو الاستعلامات التي يتيحها، افتح النافذة "docs" أو النافذة "schema" في أرضية عمل GraphQL. وإن واجهتك صعوبة في استخدام المعرف id مثل متغير في الاستعلام، توقف قليلًا لتطلع على توثيق Apollo Client بما يخص الاستعلامات. لتطلع على طريقة التوجه إلى عنوان URL في المتصفح، اقرأ توثيق Expo حول واجهة الربط البرمجية Linking API، لأنك ستحتاج ذلك عند تنفيذك للزر الذي يفتح المستودع في GitHub. ينبغي أن تمتلك واجهة العرض عنوانًا خاصًا بها، ومن الجيد أن تحدد المعرّف المميز id للمستودع ضمن المسار الذي يوجهك نحو هذا العنوان مثل معامل مسار، كي تستطيع الوصول إليه باستخدام الخطاف useParams. ينبغي أن يكون المستخدم قادرًا على الوصول إلى واجهة العرض بالضغط على المستودع ضمن قائمة المستودعات. ومن الممكن تنفيذ ذلك بتغليف المكوّن RepositoryItem داخل المكّون البنيوي Pressable في المكوّن RepositoryList، ثم استخدام الدالة navigate لتغيير العنوان من خلال معالج الحدث onPress. استخدم الخطاف useNavigate أيضًا للوصول إلى الكائن دالة navigate. ستبدو النسخة النهائية لواجهة عرض مستودع واحد قريبة من الشكل التالي: 10.20: قائمة بالآراء حول مستودع بعد أن أنجزنا واجهة عرض لمستودع وحيد، سنعرض تقييمات المستخدمين ضمن هذه الواجهة. ستجد التقييمات في الحقل reviews العائد للنوع "Repository" في تخطيط GraphQL. تشكل التقييمات قائمةً صغيرةً مرقمةً يمكن الحصول عليها باستخدام الاستعلام repositories. وإليك مثالًا عن طريقة إحضار قائمة التقييمات لمستودع واحد: { repository(id: "jaredpalmer.formik") { id fullName reviews { edges { node { id text rating createdAt user { id username } } } } } } يحتوي الحقل text لكل تقييم على رأي المستخدم للنص، ويحتوي الحقل rating على قيمة عددية بين 0 و100، أما الحقل craetedAt فيحتوي على بيانات إنشاء هذا التقييم، وأخيرًا يحتوي الحقل user معلومات عن مُنشئ التقييم ويحمل النوع "User". نريد أن نعرض التقييمات على شكل شريط تمرير، مما يجعل المكوّن البنيوي FlatList ملائمًا لأداء المهمة. ولكي تعرض معلومات مستودع محدد في أعلى الواجهة، استخدم الخاصية ListHeaderComponent للمكون FlatList. يمكنك أيضًا استخدام المكوّن البنيوي ItemSeparatorComponent لإضافة مساحة بيضاء بين العناصر المعروضة كما هو الحال بين عناصر المكوّن RepositoryList مثلًا. إليك مثالًا عن هيكلية التنفيذ: const RepositoryInfo = ({ repository }) => { // Repository's information implemented in the previous exercise }; const ReviewItem = ({ review }) => { // Single review item }; const SingleRepository = () => { // ... return ( <FlatList data={reviews} renderItem={({ item }) => <ReviewItem review={item} />} keyExtractor={({ id }) => id} ListHeaderComponent={() => <RepositoryInfo repository={repository} />} // ... /> ); }; export default SingleRepository; ستبدو النسخة النهائية عن تطبيق قائمة المستودعات مشابهة للشكل التالي: البيانات التي تراها تحت اسم المستخدم لمنشئ التقييم هي تاريخ الإنشاء الذي نجده في الحقل createdAt وهو من النوع "Review"، وينبغي أن يكون تنسيق البيانات مريحًا للمستخدم مثل الصيغة "يوم.شهر.سنة". قد يساعدك تثبيت المكتبة date-fns على تنسيق تاريخ الإنشاء بالاستفادة من الدالة format. يمكن إنجاز الحاوية ذات الشكل الدائري باستخدام خاصية التنسيق borderRadius. ولتفعل هذا عليك تثبيت قيمتي الخاصيتين width و height، ثم ضبط خاصية التنسيق border-radius عند القيمة "width/2". 10.21: نموذج التقييم أنجز نموذجًا لإنشاء تقييم جديد لمستودع مستخدمًا المكتبة Formik. ينبغي أن يضم النموذج أربعة حقول هي: اسم دخول مالك المستودع. اسم المستودع. التقييم على هيئة قيمة عددية. رأي المستخدم على هيئة نص. تحقق من الحقول مستخدمًا تخطيط Yup، بحيث تتأكد أن: اسم المستخدم لمالك المستودع هو قيمة نصية إجبارية. اسم المستودع هو قيمة نصية إجبارية. التقييم هو قيمة عددية إجبارية بين 0 و100. رأي المستخدم هو قيمة نصية اختيارية. اطلع على توثيق Yup لإيجاد المُقيَّمات validators المناسبة، واستخدم رسائل خطأ مناسبة عند استخدامك لهذه المُقيِّمات، إذ يمكنك تعريف رسالة الخطأ مثل وسيط للتابع message العائد للمقيّم. يمكنك أيضًا توسيع نص الحقل ليمتد على عدة أسطر باستعمال الخاصية multiline العائدة للمكون TextInput. استخدم الطفرة createReview لإنشاء تقييم جديد، واطلع على وسطاء الطفرة بفتح إحدى النافذتين "docs" أو "schema " في أرضية عمل GraphQL. ويمكنك استخدام الخطاف useMutation لإرسال الطفرة إلى خادم Apollo. بعد نجاح عمل الطفرة createReview، حوّل المستخدم إلى واجهة عرض المستودع التي أنجزناها في التمرين السابق. نفّذ ذلك باستخدام التابع navigate بعد أن تستخرج كائن التاريخ باستخدام الخطاف useNavigate. يمتلك التقييم الذي أنشأته حقلًا باسم "repositoryId" يمكنك استخدامه لتأسيس مسار لعنوان التقييم. استخدم سياسة الإحضار "cache-and-network" لتمنع جلب بيانات الذاكرة المؤقتة مع الاستعلام repository في واجهة عرض مستودع وحيد. يمكنك استخدامها مع الخطاف على النحو التالي: useQuery(GET_REPOSITORY, { fetchPolicy: 'cache-and-network', // خيارات أخرى }); انتبه إلى أن التقييم سيكون فقط لمستودعات GitHub العامة الموجودة فعلًا، وأن المستخدم سيقيَّم مستودعًا محددًا مرة واحدة فقط. ليس عليك الآن التعامل مع حالات الخطأ هذه، بل فقط إظهار الخطأ مع الرمز المحدد والرسالة المناسبة. جرّب ما نفّذته على أحد المستودعات العامة التي تملكها، أو أية مستودعات عامة أخرى. ينبغي أن يتمكن المستخدم من الوصول إلى واجهة التقييم من خلال شريط التطبيق. لذلك أنشئ نافذة جديدة عنوانها "Create a review". ينبغي أن تكون النافذة مرئية للمستخدم الذي سجل دخوله فقط. وعليك أن تحدد أيضًا عنوانًا لواجهة التقييم. ستبدو النسخة النهائية للتطبيق مشابهة للشكل التالي: التُقطت شاشة التطبيق هذه بعد إخفاق إرسال بيانات نموذج لإظهار شكله في هذه الحالة. 10.22: نموذج تسجيل مستخدم جديد أنجز نموذجًا لتسجيل مستخدم جديد مستخدمًا Formik. ينبغي أن يضم النموذج ثلاثة حقول: اسم مستخدم، كلمة مرور، وتأكيدًا لكلمة المرور. تحقق من صحة البيانات في النموذج مستخدمًا تخطيط Yup ومتبعًا مايلي: اسم المستخدم هو سلسلة نصية إجبارية طولها بين 1 و 30. كلمة المرور هي سلسلة نصية إجبارية طولها بين 1 و 5. تأكيد كلمة المرور يتطابق تمامًا مع كلمة المرور. قد يربكك قليلًا تأكيد كلمة المرور، لكن يمكن إنجاز الأمر بالاستفادة من التابعين oneOf وref كما هو مبين في إرشادات هذا المشروع. يمكنك إنشاء مستخدم جديدة باستخدام الطفرة createUser، ويمكنك الإطلاع على كيفية استخدامها من خلال توثيق أرضية عمل Apollo. سجّل دخول المستخدم الجديد بعد إنشاء حسابه الخاص مستخدمًا الخطاف useSignIn كما فعلنا في نموذج تسجيل الدخول. وجِّه المستخدم الجديد بعد تسجيل دخوله إلى واجهة عرض المستودعات المقيّمة. ينبغي أن يكون المستخدم قادرًا على الوصول إلى واجهة تسجيل مستخدم جديد من خلال شريط التطبيق، وذلك بالضغط على نافذة "Sign up" التي ستظهر فقط للمستخدم قبل أن يسجّل دخوله. ستبدو النسخة النهائية لواجهة تسجيل مستخدم جديد مشابهة للصورة التالية: التُقطت شاشة التطبيق هذه بعد إخفاق إرسال بيانات نموذج لإظهار شكله في هذه الحالة. 10.23: ترتيب بيانات تطبيق قائمة المستودعات المقّيمة تُرتّب المستودعات حتى هذه اللحظة وفقًا لتاريخ تقييم المستودع. أنجز آلية تسمح للمستخدم أن يختار أساسًا لترتيب هذه المستودعات وفقًا لما يلي: المستودعات الأخيرة: سيكون المستودع الذي قُيَّم آخيرًا وللمرة الأولى في أعلى القائمة. هذا الترتيب هو المتبع حاليًا، وينبغي أن يكون الخيار الإفتراضي. المستودعات الأعلى تقييمًا: سيكون المستودع الذي يحمل أعلى قيمة لمعدّل التقييم في أعلى القائمة. المستودعات الأدنى تقييمًا: سيكون المستودع الذي يحمل أدنى قيمة لمعدّل التقييم في أعلى القائمة. يمتلك الاستعلام repositories الذي يحضر قائمة المستودعات وسيطًا يدعى orderby يمكنك استخدامه لتحديد أسلوب الترتيب المتبع، إذ يمتلك هذه الوسيط قيمتين فقط هما: CREATED_AT: الترتيب وفق تاريخ التقييم لأول مرة. RATING_AVERAGE: الترتيب على أساس معدل التقييم. كما يمتلك الاستعلام وسيطًا يدعى orderDirection ويستخدم لتغيير جهة الترتيب. يملك الوسيط الأخير قيمتين، هما: ASC (تصاعدي، من التقييم الأدنى إلى الأعلى) و DESC (تنازلي، من التقييم الأعلى إلى الأدنى). يمكن المحافظة على خيار الترتيب باستخدام الخطاف useState على سبيل المثال، كما يمكن تمرير المتغيرات التي يستخدمها الاستعلام repositories إلى الخطاف useRepositories مثل وسطاء. يمكنك أيضًا استخدام المكتبة react-native-picker أو المكوّن Menu العائد للمكتبة React Native Paper لإنجاز شيفرة ترتيب القائمة، كما يمكنك استخدام الخاصية ListHeaderComponent العائدة للمكوّن FlatList لتزويدك بترويسة تحتوي على مكوّن الاختيار. ستبدو النسخة النهائية لهذه الميزة، وبناء على مكوَّن الاختيار الذي اعتمدته، قريبةً من الصورة التالية: 10.24: انتقاء مستودعات محددة من القائمة يتيح خادم Apollo ترشيح المستودعات بناءً على اسمها أو اسم مالكها، ويمكن إنجاز ذلك باستخدام الوسيط searchKeyword العائد للاستعلام repositories. إليك مثالًا عن كيفية استخدام الوسيط مع الاستعلام: { repositories(searchKeyword: "ze") { edges { node { id fullName } } } } أنجز ميزةً في التطبيق لترشيح قائمة المستودعات المقيّمة بناءً على كلمة محددة. ينبغي أن يكون المستخدم قادرًا على الكتابة ضمن مربع إدخال نصي وستظهر نتائج الفلترة وفقًا للكلمة المكتوبة مباشرة أثناء الكتابة. يمكنك استخدام المكوّن البسيط TextInput، أو استخدام مكوّن أكثر عصرية مثل المكوّن Searchbar العائد للمكتبة React Native Paper على أنه مربع إدخال نصي. ضع مكوّن الإدخال النصي ضمن ترويسة المكوّن FlatList. ولتفادي تنفيذ عدد كبير من الاستعلامات أثناء كتابة المستخدم للكلمة بسرعة، اختر آخر سلسة كتبها بعد تأخير بسيط. تدعى هذه التقنية بالتريّث debouncing. تمثّل المكتبة use-debounce حلًا جيدًا لتأخير متغيّر الحالة، بعد أن تستخدم زمن انتظار معقول مثل 500 ميلي ثانية. خزّن قيمة النص الذي يُكتب في مربع الإدخال مستخدمًا الخطاف useState، ومرر القيمة المُؤخَّرة إلى الاستعلام كقيمة للوسيط searchKeyword. قد تعترضك مشكلة فقدان مكون الإدخال النصي تركيز الدخل بعد كتابة كل حرف، وذلك لأن المحتوى الذي تقدّمه الخاصية ListHeaderComponent سيُلغى باستمرار. يمكن حل المشكلة بتحويل المكوّن المسؤول عن تصيير المكوّن FlatList إلى مكوّن صنف class component ومن ثم تعريف دالة تصيير الترويسة إلى خاصية للصنف على النحو التالي: export class RepositoryListContainer extends React.Component { renderHeader = () => { // يحوي خاصيات المكون this.props const props = this.props; // ... return ( <RepositoryListHeader // ... /> ); }; render() { return ( <FlatList // ... ListHeaderComponent={this.renderHeader} /> ); } } ستبدو النسخة النهائية لميزة انتقاء المستودعات قريبةً من الصورة التالية: الترقيم بطريقة المؤشرات عندما تعيد الواجهة البرمجية قائمةً مرتبةً من العناصر من مجموعةٍ ما، فإنها ستعيد مجموعةً جزئيةً من العناصر من أصل المجموعة الكاملة لتقليل عرض حزمة الاتصال مع الخادم وتقليل الذاكرة المستخدمة لتخزين القائمة في تطبيق المستخدم. يمكن أن نطلب تلك المجموعة الجزئية لتوافق معاملات محددة لكي يستطيع المستخدم أن يطلب مثلًا أول عشرين عنصرًا منها ابتداءًا من عنصر محدد. تدعى هذه التقنية عادة بالترقيم pagination. وعندما نستطيع الحصول على قائمة بعد عنصر محدد بمؤشر معيّن، سنكون أمام الترقيم المبني على المؤشرات cursor-based pagination. فالمؤشر إذًا هو تحديدٌ لعنصر في قائمة مرتبة. لنلقي نظرةً على قائمة المستودعات المرقمة التي يعيدها الاستعلام repositories باستخدام الاستعلام التالي: { repositories(first: 2) { totalCount edges { node { id fullName createdAt } cursor } pageInfo { endCursor startCursor hasNextPage } } } يبلِّغ الوسيط first الواجهة البرمجية ان تعيد فقط أول مستودعين. وإليك مثالًا عن الاستجابة لهذا الاستعلام: { "data": { "repositories": { "totalCount": 10, "edges": [ { "node": { "id": "zeit.next.js", "fullName": "zeit/next.js", "createdAt": "2020-05-15T11:59:57.557Z" }, "cursor": "WyJ6ZWl0Lm5leHQuanMiLDE1ODk1NDM5OTc1NTdd" }, { "node": { "id": "zeit.swr", "fullName": "zeit/swr", "createdAt": "2020-05-15T11:58:53.867Z" }, "cursor": "WyJ6ZWl0LnN3ciIsMTU4OTU0MzkzMzg2N10=" } ], "pageInfo": { "endCursor": "WyJ6ZWl0LnN3ciIsMTU4OTU0MzkzMzg2N10=", "startCursor": "WyJ6ZWl0Lm5leHQuanMiLDE1ODk1NDM5OTc1NTdd", "hasNextPage": true } } } } يعتمد تنسيق الكائن الناتج والوسطاء على مواصفات اتصالات مؤشر أرضية عمل GraphQL للترحيل، والتي أصبحت مواصفات ترقيم صفحات شائعة جدًا واعتُمدت على نطاقٍ واسع في واجهة برمجة تطبيقات GitHub's GraphQL API. سيحتوي الكائن الذي يحمل النتيجة مصفوفةً باسم edges تحتوي بدورها على عناصر لها سمتين الأولى هي العقدة node والأخرى هي المؤشر cursor؛ إذ تحتوي العقدة على المستودع بحد ذاته كما نعرف؛ أما المؤشر فهو تمثيلٌ مشفّر للعقدة بأسلوب "Base64". ويحتوي المؤشر على المعرّف id للمستودع بالإضافة إلى تاريخ إنشائه. هذه هي كل المعلومات التي نريدها لكي نشير إلى العنصر عندما تُرتَّب العناصر وفق تاريخ إنشاء المستودع. يحتوي الكائن pageInfoعلى معلومات مثل المؤشرات على بداية ونهاية المصفوفة. دعونا نتأمل الحالة التي نريد فيها مجموعة العناصر التي تلي آخر عنصر من المجموعة الحالية وهو العنصر "zeit/swr". يمكننا إسناد قيمة الحقل endCursor إلى الوسيط after للاستعلام على النحو التالي: { repositories(first: 2, after: "WyJ6ZWl0LnN3ciIsMTU4OTU0MzkzMzg2N10=") { totalCount edges { node { id fullName createdAt } cursor } pageInfo { endCursor startCursor hasNextPage } } } والآن سنحصل على العنصرين التاليين، وسنستمر في هذه العملية حتى يأخذ الحقل hasNextPage القيمة false، وهذا يعني أننا وصلنا آخر القائمة، ولتتعمق أكثر في الترقيم المتعلق بالمؤشرات، إقرأ المقالة التي عنوانها Pagination with Relative Cursors على موقع Shopify، حيث تقدم المقالة الكثير من التفاصيل عن طريقة كتابة الشيفرة بالإضافة إلى الفوائد من استخدام الترقيم بالمؤشرات مقارنةً بالترقيم وفق ترتيب العنصر index-based. التمرير اللانهائي تُصمّم القوائم المرتبة عموديًا في تطبيقات سطح المكتب أو تطبيقات الهواتف الذكية بتقنية تدعى التمرير اللانهائي - infinite scrolling. ويعتمد مبدأ هذه التقنية على النقاط التالية: إحضار المجموعة الأولية من العناصر. عند وصول المستخدم إلى آخر عنصر من هذه المجموعة، يجري إحضار المجموعة التالية من العناصر التي تبدأ من العنصر الذي يلي آخر عنصر من المجموعة السابقة. تتكرر الخطوة الثانية حتى يتوقف المستخدم عن تمرير شريط القائمة أو عندما يتجاوز حدًا معينًا من مرات التمرير. ويشير الاسم "تمرير لانهائي" إلى أن القائمة ستبدو لانهائية، فسيبقى المستخدم قادرًا على تمرير العناصر ضمن الشريط وستُعرض العناصر بصورةٍ مستمرة أيضًا. لنرى كيف سننفذ ذلك عمليًا من خلال خطاف Apollo Client الذي يُدعى useQuery. يضم توثيق Apollo Client تفاصيل كثيرة عن طريقة تنفيذ الترقيم باستخدام المؤشرات. لننجز إذًا ميزة التمرير اللانهائي لعرض قائمة المستودعات المقيّمة. علينا أولًا أن نحدد متى يصل المستخدم إلى آخر عنصر في القائمة. ولحسن الحظ، يمتلك المكون FlatList الخاصية onEndReached التي تستدعى الدالة التي ستنفذ العملية عندما يصل المستخدم إلى آخر عنصر من القائمة. يمكنك التحكم بوقت استدعاء الدالة onEndReach باستخدام الخاصية onEndReachedThreshold. غيِّر شيفرة المكوّن FlatList الموجود ضمن المكوّن RepositoryList لكي يستدعي الدالة حالما يصل المستخدم إلى العنصر الأخير: export const RepositoryListContainer = ({ repositories, onEndReach, /* ... */, }) => { const repositoryNodes = repositories ? repositories.edges.map((edge) => edge.node) : []; return ( <FlatList data={repositoryNodes} // ... onEndReached={onEndReach} onEndReachedThreshold={0.5} /> ); }; const RepositoryList = () => { // ... const { repositories } = useRepositories(/* ... */); const onEndReach = () => { console.log('You have reached the end of the list'); }; return ( <RepositoryListContainer repositories={repositories} onEndReach={onEndReach} // ... /> ); }; export default RepositoryList; جرّب عملية التمرير إلى نهاية قائمة المستودعات، وستظهر رسالةً في سجل العمل مفادها أنك وصلت إلى العنصر الأخير. علينا الآن أن نحضر مزيدًا من المستودعات في اللحظة التي نصل فيها إلى نهاية العناصر. يمكن إنجاز ذلك باستخدام الدالة fetchMore التي يؤمنها الخطاف useQuery. يمكننا استخدام سياسة الحقل field policy لوصف عميل Apollo وكيفية دمج المستودعات الموجودة في ذاكرة التخزين المؤقت مع المجموعة التالية من المستودعات، إذ تُستخدم سياسات الحقول عمومًا لتخصيص سلوك ذاكرة التخزين المؤقت أثناء عمليات القراءة والكتابة باستخدام دوال القراءة والدمج. دعنا نضيف سياسة حقل للاستعلام repositories في ملف "apolloClient.js" على النحو التالي: import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; import Constants from 'expo-constants'; import { relayStylePagination } from '@apollo/client/utilities'; const { apolloUri } = Constants.manifest.extra; const httpLink = createHttpLink({ uri: apolloUri, }); const cache = new InMemoryCache({ typePolicies: { Query: { fields: { repositories: relayStylePagination(), }, }, }, }); const createApolloClient = (authStorage) => { const authLink = setContext(async (_, { headers }) => { try { const accessToken = await authStorage.getAccessToken(); return { headers: { ...headers, authorization: accessToken ? `Bearer ${accessToken}` : '', }, }; } catch (e) { console.log(e); return { headers, }; } }); return new ApolloClient({ link: authLink.concat(httpLink), cache, }); }; export default createApolloClient; كما ذكرنا سابقًا، يعتمد تنسيق كائن نتيجة ترقيم الصفحات والوسطاء على مواصفات ترقيم الصفحات الخاصة بالترحيل Relay، ولحسن الحظ، يوفر Apollo Client سياسة حقل محددة مسبقًا، relayStylePagination، والتي يمكن استخدامها في هذه الحالة. غيّر في شيفرة الخطاف useRepositories بحيث يعيد الدالة fetchMore بعد فَكِّ تشفيرها، بحيث تستدعي الدالة fetchMore الفعلية مع الحقل endCursor، ثم يُحدّث بعدها الاستعلام بصورة صحيحة وفقًا للبيانات المُحضرة: const useRepositories = (variables) => { const { data, loading, fetchMore, ...result } = useQuery(GET_REPOSITORIES, { variables, // ... }); const handleFetchMore = () => { const canFetchMore = !loading && data?.repositories.pageInfo.hasNextPage; if (!canFetchMore) { return; } fetchMore({ variables: { after: data.repositories.pageInfo.endCursor, ...variables, }, }); }; return { repositories: data?.repositories, fetchMore: handleFetchMore, loading, ...result, }; }; تأكد من وجود الحقلين pageInfo و cursor في الاستعلام repositories كما هو موضح في مثال الترقيم. كما ينبغي عليك أن تضيف الوسيطين first و after إلى الاستعلام. ستستدعي الدالة handleFetchMore الدالة fetchMore العائدة إلى Apollo client إن كان هناك المزيد من العناصر التي ينبغي إحضارها، وذلك بناء على قيمة الخاصية hasNextPage، ومن المفترض أن نمنع إحضار عناصر جديدة طالما أن عملية الإحضار سابقةً لا تزال قيد التنفيذ، إذ ستكون قيمة الحقل loading في هذه الحالة true. سنزود الاستعلام في الدالة fetchMore بالمتغير after الذي يتلقى آخر قيمة endCursor للحقل. وتكون الخطوة الأخيرة استدعاء الدالة fetchMore ضمن دالة معالجة الحدث onEndReach: const RepositoryList = () => { // ... const { repositories, fetchMore } = useRepositories({ first: 8, // ... }); const onEndReach = () => { fetchMore(); }; return ( <RepositoryListContainer repositories={repositories} onEndReach={onEndReach} // ... /> ); }; export default RepositoryList; استخدم قيمة صغيرة نسبيًا (8 مثًلا) للوسيط first عندما تجرب التمرير اللانهائي، وهكذا لن تضطر إلى عرض كثيرٍ من المستودعات، وقد تواجهك أيضًا مشكلة الاستدعاء المباشر لمعالج الحدث onEndReach بعد تحميل واجهة العرض، والسبب على الأرجح، هو العدد الكبير للمستودعات في القائمة، وبالتالي ستصل إلى نهاية القائمة مباشرةً. يمكنك الالتفاف على هذه المشكلة بزيادة قيمة الوسيط first. وبمجرد أن ترى أن قائمة التمرير اللانهائي تعمل جيدًا، يمكنك تجريب قيم كبيرة للوسيط first. التمرينات 10.25- 10.27 10.15: شريط تمرير لانهائي لقائمة المستودعات المقيّمة نفذ شريط تمرير لانهائي لقائمة المستودعات المقيّمة. يمتلك الحقل reviews العائد للنوع "Repository" الوسيطينafter و first كما في الاستعلام repositories. ويمتلك النوع "ReviewConnection" الحقل pageInfo كما يمتلكه النوع "RepositoryConnection". إليك مثالًا عن استعلام: { repository(id: "jaredpalmer.formik") { id fullName reviews(first: 2, after: "WyIxYjEwZTRkOC01N2VlLTRkMDAtODg4Ni1lNGEwNDlkN2ZmOGYuamFyZWRwYWxtZXIuZm9ybWlrIiwxNTg4NjU2NzUwMDgwXQ==") { totalCount edges { node { id text rating createdAt repositoryId user { id username } } cursor } pageInfo { endCursor startCursor hasNextPage } } } } يمكن أن تتشابه سياسة حقل ذاكرة التخزين المؤقت مع استعلام repositories: const cache = new InMemoryCache({ typePolicies: { Query: { fields: { repositories: relayStylePagination(), }, }, Repository: { fields: { reviews: relayStylePagination(), }, }, }, }); استخدم قيمةً صغيرةً نسبيًا للوسيط first عندما تحاول إنجاز قائمة التمرير اللانهائي، وربما ستضطر إلى إنشاء عدة مستخدمين جدد لكي تُقيّم عددًا من المستودعات، وبذلك تصبح قائمة المستودعات المقيّمة طويلة بما يكفي لتجرّب شريط التمرير. اجعل قيمة الوسيط كبيرةً بما يكفي لعدم استدعاء معالج الحدث onEndReach مباشرةً عند عرض الواجهة، وصغيرةً في نفس الوقت بحيث يعرض الشريط المستودعات من جديد عند الوصول إلى نهاية القائمة. وبمجرد أن ترى أن قائمة التمرير اللانهائي تعمل جيدًا، يمكنك تجريب قيم كبيرة للوسيط first. 10.26: واجهة عرض لتقييمات المستخدم أنجز واجهة تُمكِّن المستخدم من عرض ما قيّمه. ينبغي أن يكون المستخدم قادرًا على الوصول إلى هذه الواجهة من خلال الضغط على النافذة "My reviews" في شريط التطبيق بمجرد أن يسّجل دخوله. يمكنك تطبيق أسلوب التمرير اللانهائي إن أردت في هذا التمرين. وإليك الشكل الذي يمكن أن تظهر عليه هذه الواجهة: تذكر أنه بإمكانك الحصول على المستخدم الذي سجّل دخول من خادم Apollo مستعينًا بالاستعلام me. سيعيد الاستعلام النوع "User" الذي يمتلك الحقل review. إن كنت قد أنجزت مسبقًا الاستعلام me يمكنك إعادة استخدامه بعد جعله قادرًا على إحضار الحقل review شرطيWh، وذلك بالاستفادة من توجيه GraphQL الذي يُدعى include. لنفترض أن الاستعلام الحالي قد أنجز بصورةٍ قريبة من التالي: const GET_CURRENT_USER = gql` query { me { # user fields... } } `; يمكنك تزويد الاستعلام بالوسيط includeReviewواستخدام ذلك مع التوجيه include: const GET_CURRENT_USER = gql` query getCurrentUser($includeReviews: Boolean = false) { me { # user fields... reviews @include(if: $includeReviews) { edges { node { # review fields... } cursor } pageInfo { # page info fields... } } } } `; يمتلك الوسيط includeReview القيمة الافتراضية false، لأننا لا نريد أن نسبب حملًا زائدًا على الخادم ما لم نرد صراحةً إحضار المستودعات التي قيّمها المستخدم. إن مبدأ هذا التوجيه بسيط على النحو التالي: إن كانت قيم الوسيط if هي true، أحضر الحقل، وإلا احذفه. 10.27: إضافة أفعال إلى واجهة عرض التقييمات بعد أن أصبح المستخدم قادرًا على استعراض ما قيّمه، لنضف إلى الواجهة بعض الأفعال. ضع أسفل كل تقيم زرين، أحدهما لعرض المستودع، بحيث ينتقل المستخدم بالضغط عليه إلى واجهة عرض مستودع واحد، والآخر لحذف تقييم هذا المستودع. إليك ما قد يبدو عليه الوضع بعد إضافة الزرين: ينبغي أن يلي الضغط على زر الحذف رسالة تأكيد للحذف، فإن أكد المستخدم أمر الحذف ستُنفّذ العملية، وإلا سيُهمل الأمر. يمكنك إنجاز ذلك باستخدام الوحدة البرمجية Alert. وانتبه إلى أن استدعاء التابع Alert.alert لن يفتح رسالة التأكيد في واجهة عرض المنصة Expo، بل عليك استخدام تطبيق جوّال Expo أو المقلّد لترى كيف ستبدو هذه الرسالة. إليك الرسالة التي ينبغي أن تظهر عند الضغط على زر الحذف: يمكنك حذف التقييم باستخدام الطفرة deleteReview. تمتلك هذه الطفرة وسيطًا واحدًا وهو المعرّف id للتقييم الذي سيُحذف. ومن السهل بعد تنفيذ الطفرة تحديث استعلام قائمة المستودعات باستدعاء الدالة refetch. هكذا نكون قد وصلنا إلى التمرين الأخير في هذا المقال، وقد حان وقت تسليم الإجابات إلى GitHub والإشارة إلى التمارين التي أكملتها في منظومة تسيم التمارين. انتبه إلى وضع تمارين هذا المقال في القسم 4 من منظومة التسليم. مصادر إضافية طالما أننا وصلنا إلى نهاية هذا القسم، دعونا نلقي نظرةً على بعض المصادر الهامة للتطوير باستخدام React Native. سنبدأ من توثيق React Native باللغة العربية الذي تقدمه أكاديمية حسوب، والتي تقدم أيضًا توثيق React باللغة العربية، بالإضافة إلى القسم المتخصص باللغة React على موقع الأكاديمية، كما نشير إلى الموقع Awesome React Native الذي يقدم قائمةً مهمةً جدًا من المصادر مثل المكتبات والدورات التعليمية. وبما أن القائمة طويلة جدًا، لنطلع على بعض النقاط. المكتبة React Native Paper المكتبة Paper هي مجموعةٌ من المكوّنات المخصصة والجاهزة للاستخدام في React Native، مبنيةٌ على معايير تصميم Material التي وضعتها غوغل Google. صُممت React Native Paper من أجل لتقدم وظيفة تماثل وظائف المكتبة Material-UI، إذ تقدم مجموعةً واسعةً من مكوّنات واجهة المستخدم UI عالية الجودة التي تدعم إنشاء سمات مخصصة للتطبيق. وتتميز إعدادات استخدام React Native Paper مع تطبيقات React Native المبنية على منصة Expo بالسهولة، حيث يمكنك تجربتها مباشرة في التمارين القادمة إن أردت. المكتبة Styled-components سيعطيك استخدام القوالب المعرّفة المجرّدة tagged template literal -وهي إضافةٌ جديدة في JavaScript-، مع المكتبة styled-components إمكانية كتابة شيفرة CSS فعلية لتنسيق المكوّنات، كما يلغي استخدام هذه المكتبة الحاجة إلى الربط بين المكوّنات والتنسيق، وسيكون استخدام المكوّنات مثل وحدات بناءً تنسيقات على المستوى البرمجي المنخفض سهلًا جدًا. تستخدم المكتبة Styled-components لتنسيق مكوّنات React Native باستخدام تقنية CSS-in-JS. لقد اعتدنا على تعريف تنسيق المكوّنات في React Native باستخدام كائنات JavaScript، وبالتالي لن يكون استخدام CSS-in-JS غريبًا. لكن مقاربة المكتبة Styled-components ستختلف تمامًا عن استخدام التابع StyleSheet.createو الخاصية style. يُعرّف تسيق المكوّن في Styled-components باستخدام ميزة تُدعى القوالب المعرّفة المجرّدة tagged template literal أو كائنات جافا سكربت JavaScript الصِّرفة، التي تُمكننا من تعريف خصائص تنسيق جديدة للمكونات بناءً على خصائص تلك المكوّنات في زمن التنفيذ. سيتيح ذلك الكثير من الإمكانات مثل الانتقال بين السمات القاتمة والمضيئة، كما أنها تدعم كليًا موضوع السمات. إليك مثالًا عن إنشاء المكوّن Text مع إمكانية تغيير تنسيقه بناءً على خصائصه: import styled from 'styled-components/native'; import { css } from 'styled-components'; const FancyText = styled.Text` color: grey; font-size: 14px; ${({ isBlue }) => isBlue && css` color: blue; `} ${({ isBig }) => isBig && css` font-size: 24px; font-weight: 700; `} `; const Main = () => { return ( <> <FancyText>Simple text</FancyText> <FancyText isBlue>Blue text</FancyText> <FancyText isBig>Big text</FancyText> <FancyText isBig isBlue> Big blue text </FancyText> </> ); }; وطالما أن المكتبة styled-components ستعالج تعريف التنسيق، يمكننا استخدام أسلوب كتابة الأفعى snake case المشابه لطريقة كتابة CSS لتسمية الخصائص والواحدات (مثل % أو px)، لكن لا أهمية في الواقع للواحدات، لأن قيم خصائص التنسيق لا تقبل ذلك. للاطلاع على مزيدٍ من المعلومات عن استخدام هذه المكتبة، اقرأ التوثيق الخاص بها. المكتبة React-spring react-spring هي مكتبة رسوم متحركة أساسها المكتبة spring-physics، تغطي معظم احتياجاتك لواجهات مزودة برسوم متحركة، إذ تؤمن لك هذه المكتبة أدوات بالمرونة الكافية لتحويل جميع أفكارك إلى واجهات متحركة. تؤمن لك المكتبة React-spring واجهة برمجية ذات خطافات hook API واضحة الاستخدام لتحريك مكونات React Native. المكتبة React Navigation تؤمن مكتبة React Navigation التنقل والتوجه في تطبيقات React Native هي مكتبةٌ للتحكم بالتنقلات بين وجهات مختلفة في تطبيقات React Native. تتشابه في بعض النواحي مع المكتبة React Router. لكن وعلى خلاف React Router، تقدم مميزات أكثر قربًا للمنصة الأصيلة مثل الإيماءات الأصيلة والرسوميات الانتقالية التي تظهر عند التنقل بين الواجهات المختلفة للتطبيق. كلمة ختامية وهكذا نكون قد أكملنا تطبيقنا وأصبح جاهزًا. لقد تعلمنا خلال هذه الرحلة العديد من المفاهيم الجديدة، مثل إعداد تطبيقات React Native باستخدام المنصة Expo، وطريقة العمل مع مكوّنات React Native البنيوية، وتنسيق مظهر هذه المكوّنات، إضافةً إلى الاتصال مع الخادم واختبار التطبيقات. ستكون الخطوة الأخيرة هي نشر التطبيق على متجر Apple App أو متجر Google Play. يبقى موضوع نشر التطبيق خيارًا شخصيًا وليس أمرًا مُلحًّا، لأنك ستحتاج أيضًا إلى نشر وتوزيع الخادم rate-repository-api، أما من أجل تطبيق React Native بحد ذاته، فعليك أن تُنشئ نسخة بناء لمنصة iOS أو لمنصة أندرويد باتباع الإرشادات في توثيق Expo. وبعد ذلك ستتمكن من توزيع هذه النسخ على متجري Apple App أو Google Play، وستجد تفاصيل عن هذه الخطوة أيضًا في توثيق Expo. ترجمة -وبتصرف- للفصل Testing and extending our application من سلسلة [Deep Dive Into Modern Web Development اقرأ أيضًا المقال السابق: تواصل تطبيق React Native مع الخادم أساسيات React Native مدخل إلى التحريك في React Native
  25. إن أهم ما يميز أنظمة التشغيل من ناحية سهولة الاستخدام (وخاصةً للمبتدئين والمستخدمين العاديين)، هي طريقة إدارة النظام للمجلدات والملفات مثل طريقة عرضها وإنشائها والتحكم بموقعها وخصائصها وطريقة الوصول إليها. لهذا تُعد اﻷنظمة التي تُقدّم واجهة رسومية GUI مثل أوبونتو وويندوز وماك أكثرها انتشارًا واستخدامًا؛ وإن كانت هناك اختلافات بينها، وميزات إيجابية وسلبية تتفاوت بين نظام وآخر. نقدم في مقالنا هذا اﻷفكار اﻷساسية المتعلقة بنظام إدارة المجلدات والملفات في أوبونتو، وذلك لكي تتمكن من التعامل بحرية مع الملفات التي تُنشئها وتنظيمها بالطريقة التي تراها مناسبة؛ كما سنلقي نظرةً على وسائط التخزين الداخلية والخارجية، وفكرة تثبيتها Mounting وإزالتها. ستكون قادرًا في نهاية هذا المقال على استثمار حاسوبك بطريقة مريحة ومثمرة، ومتمكنًا من تنظيم ملفاتك والعمل معها بكل ثقة. النافذة الرئيسية "منزل" وفكرة المجلد الجذري تُبنى هيكلية تنظيم المجلدات والملفات في أوبونتو على فكرة وجود مجلد جذري root folder تتفرع عنه بقية المجلدات. يُعرف هذا المجلد باسم "home/"، ولايوجد ضمنه سوى مجلد وحيد يحمل اسم مستخدم الجهاز، كما يُعرف باسم "المنزل". لا يمكن أن تضع في المجلد الجذري أي مجلدات أخرى أو أية ملفات؛ وبالتالي سيبدأ مجال عملك من "المنزل" وما داخله. ولكي ننتقل مباشرةً إلى النافذة الرئيسية "منزل"، يمكنك النقر نقرةً مزدوجةً على المجلد الذي يحمل اسم المستخدم ضمن نافذة سطح المكتب، أو النقر على تطبيق "الملفات" من شريط التطبيقات. أقسام النافذة "منزل" كمثال عن نوافذ واجهة جنوم الرسومية توضح لقطة الشاشة التالية النافذة الرئيسية "منزل"، واﻷقسام التي تتألف منها عمومًا: تتكون النافذة من: الشريط الجانبي: يقع على يمين النافذة في النسخة العربية ويضم اختصارات للوصول إلى المجلدات اﻷساسية إين ما كنت، باﻹضافة إلى اختصارات للوصول إلى سواقات الأقراص المدمجة وأجهزة التخزين الخارجية وأماكن أخرى كالمجلدات المشتركة على الشبكات. فضاءالعمل: وهي المساحة البيضاء التي تشغل معظم مساحة النافذة، وتُعرض فيها المجلدات والملفات الموجودة في المجلد الحالي. يمكنك العمل مع المجلدات الموجودة ضمن هذه النافذة من نسخ ولصق وتغيير تسمية وغيرها؛ وبالنقر على فضاء العمل نقرةً واحدةً بالزر اليميني، ستظهر لك قائمة مختصرة تتيح بعض خيارات التعامل مع المجلدات. شريط اﻷدوات: شريط أسود يقع أعلى النافذة ويضم مجموعةً من اﻷزرار التي تؤدي وظائف متنوعة. تصنف أزرار شريط الأدوات ضمن مجموعات هي: أزرار التنقل: وتضم زري "أمام" و"خلف" للانتقال إلى الملجد اﻷعلى أو اﻷدنى مستوىً من المجلد الحالي، وإلى جوارها موقع المجلد الحالي بالنسبة لمجلد "المنزل". أزرار التحكم الرئيسية بالنافذة: تقع على يمين شريط المهام وتضم على التسلسل من أقصى اليسار إلى اليمين أزرار اﻹغلاق، وتكبير أو تصغير النافذة وإخفاء النافذة. أزرار تنظيم فضاء العمل: قد يختلف عدد هذه اﻷزرار من نافذة إلى أخرى باختلاف التطبيق الذي يفتحها، لكنها تضم افتراضيًا اﻷزرار التالية مرتبة من اليسار إلى اليمين: زر البحث: بالنقر عليه، يظهر إلى يساره مربع حوار لكتابة ما تريد البحث عنه في هذا المجلد. زر تغيير المنظور: بالنقر عليه، يتبدّل تنظيم اﻷيقونات في فضاء العمل بين عرض القائمة والعرض الحر. زر خيارات المنظور: بالنقر عليه، تظهر قائمة تتيح لك مجموعة إضافية من الخيارات تتعلق بحجم اﻷيقونات المعروضة وطريقة ترتيبها وعمليات التراجع أو إعادة فعل سابق. زر القائمة: يعرض هذا الزر مجموعةً من الخيارات الهامة للعمل ضمن المجلد نستعرضها تاليًا بالتفصيل: أيقونات فتح النوافذ والمجلدات: تقع أعلى القائمة وهي بالترتيب: فتح نافذة جديدة، وفتح نافذة فرعية أخرى ضمن النافذة الواحدة، وإضافة مجلد جديد. أيقونات التحرير: وهي على الترتيب من اليسار إلى اليمين: قص ونسخ ولصق. اختيار الكل: لاختيار جميع محتويات المجلد. أظهر الملفات المخفية: ﻹظهار أية ملفات جرى إخفاؤها ضمن المجلد. أظهر الشريط الجانبي show sidebar: ﻹخفاء وإظهار الشريط الجانبي للنافذة. التفضيلات: تظهر عند النقر على هذا الخيار نافذة التفضيلات التي تضم أدوات تتحكم في طريقة عرض وفتح الملفات والبحث عنها وغيرها. اختصارات لوحة المفاتيح: وتعرض لك نافذةً تضم معظم اختصارات لوحة المفاتيح التي تحتاجها ﻷداء المهام في النوافذ. مساعدة: سيحمّل هذا الخيار تطبيق "مساعدة". عن الملفات: يعرض لمحة عن تطبيق "ملفات". أساسيات التعامل مع المجلدات والملفات بعد أن أخذنا فكرةً واضحةً عن أقسام النافذة، سننتقل مباشرةً إلى العمل مع المجلدات والملفات. إنشاء مجلد جديد وحذف مجلد وتغيير اسمه ﻹشاء مجلد جديد في نافذة، انقر بالزر الأيمن للفأرة ضمن فضاء العمل، واختر مجلد جديد أو اضغط المفاتيح Shift + Ctrl + N؛ كما يمكنك النقر على قائمة النافذة في شريط اﻷدوات، ثم النقر على أيقونة المجلد الجديد. وأيما اخترت من الطرق، ستظهر لك نافذة إنشاء مجلد جديد تضم مربح حوار يطلب إليك اختيار اسم للمجلد الجديد. انقر على الزر "أنشئ" ﻹنشاء المجلد بالاسم الذي اخترته، أو "إلغ" ﻹلغاء اﻷمر. لحذف مجلد ما، انقر على المجلد ثم اضغط على المفتاح Delete، أو انقر بالزر الأيمن للفأرة على المجلد لتظهر قائمة خيارات المجلد. انتقل بعدها إلى الخيار "انقل إلى المهملات"، وانقر عليه. في كلتا الطريقتين سيُحذف المجلد، لكن بعد أن تتم العملية، ستعرض عليك النافذة رسالةً تخبرك فيها أن المجلد قد حُذف وتعطيك خيار "تراجع" إن فعلت ذلك عن طريق الخطأ، فستختفي الرسالة إن أهملتها الرسالة تلقائيًا بعد عدة ثوان. لإعادة تسمية مجلد، انقر عليه، ثم اضغط على المفتاح F2؛ أو انقر عليه بالزر الأيمن للفأرة واختر اﻷمر "غيّر الاسم" من قائمة الخيارات المتاحة. تعرض النافذة في كلتا الطريقتين رسالةً تضم مربع حوار لكتابة الاسم الجديد، وإلى جواره زر "غيّر الاسم". إن غيرت اسم المجلد ولم تنقر على زر "غيّر الاسم"، فلن يحدث شيء. تحريك المجلدات والملفات ونقلها نسخ مجلد ولصقه: لتنشئ نسخةً جديدةً عن مجلدك، انقر على المجلد بالزر الأيمن، ثم اختر اﻷمر "انسخ"؛ كما يمكنك استخدام الاختصار Ctrl + C. عندها يخزن النظام نسخةً من مجلدك أو ملفك في ذاكرته، ويبقيها حتى تُلصق هذه النسخة في المكان المطلوب. إن أردت إنشاء نسخة في نفس النافذة، فانقر بالزر الأيمن للفأرة ضمن فضاء عمل النافذة، واختر اﻷمر "ألصق"، أو استخدم الاختصار Ctrl + V. سيظهر مجلد يحمل اسم المجلد السابق وإلى جواره عبارة "(نسخة)" إن كانت النسخة اﻷولى وعبارة "(نسخة أخرى)" إن كانت الثانية و"(3 نسخ)" إن كانت الثالثة وهكذا!؛ أما إن أردت إنشاء نسخة في نافذة أخرى، فافتح هذه النافذة وكرر خطوات اللصق السابقة. قص مجلد ولصقه: تماثل هذه العملية عملية النسخ، إلا أن النسخة اﻷصلية التي جرى قصها من مكانها ستختفي عند لصقها في مكان آخر، ولن تبقى أيضًا نسخة عنها في الحافظة، إذ لن يتفعّل الخيار "ألصق" في قائمة الخيارات. لقص ملف أو مجلد، انقر عليه بالزر الأيمن للفأرة ثم اختر اﻷمر "قص"، أو اضغط على المفتاحين Ctrl + X، ثم انتقل إلى النافذة الهدف واختر اﻷمر "ألصق". ضغط المجلدات والملفات يقدم لك نظام التشغيل أوبونتو ميزة ضغط الملفات وذلك لغرضين أساسيين: تصغير حجم المجلد أو الملف. سهولة مشاركة المجلد على اﻹنترنت نظرًا لامتناع الكثير من الخدمات عن تحميل أو تنزيل ملفات بامتدادات معينة، ونلجأ إلى تحويلها إلى صيغة ملف مضغوط. لضغط مجلد (أو كما يُعرف بإنشاء أرشيف)، انقر عليه بالزر الأيمن للفأرة، ثم اختر اﻷمر "اضغط"، وستظهر لك النافذة التالية: لاحظ وجود ثلاثة أنواع من أرشيفات الضغط هي: zip: وهو أرشيف عالي الدعم من قِبل معظم أنظمة التشغيل، ويتميز بسرعة إنشائه وفكّه. tar.xz: أرشيف تدعمه معظم توزيعات لينكس، كما توجد برامج تدعمه في ويندوز، لكنه أبطأ في اﻹنشاء والفك مقارنةً بالسابق، ويتميز بحجم أصغر. 7z.: يماثل السابق من حيث سرعة اﻹنشاء والفك والحجم الصغير، وتدعمه توزيعات لينكس؛ كما تجد مجموعة برمجيات تدعمه على ويندوز وماك. اكتب اسم اﻷرشيف الذي ستضغط فيه مجلداتك أو ملفاتك واختر نوعه ثم انقر الزر "أنشئ". خصائص الملفات والمجلدات للاطلاع على خصائص مجلد أو ملف، انقر عليه بالزر الأيمن للفأرة، ثم اختر "خصائص"؛ أو اضغط المفتاحين Ctrl + I معًا. تختلف نافذة الخصائص الظاهرة في حالتي المجلد والملف، وسنستعرض كل منهما سريعًا. خصائص المجلدات تتكون النافذة من النوافذ الفرعية التالية: أساسي: وتقدم معلومات عن نوع وموقع المجلد والمساحة الفارغة ضمن المجلد الجذري. التصاريح: وتقدم خيارات للتحكم بأذونات فتح المجلد للقراءة فقط أو للقراءة والكتابة، وهو موضوع سنتحدث فيه لاحقًا. المشاركة على الشبكة المحلية: إن كان حاسوبك متصلًا بشبكة محلية، فيمكنك عندها مشاركة المجلد مع مستخدمي الشبكة؛ وهذا موضوع خارج نطاق مقالنا. خصائص الملفات تتكون النافذة من نفس النوافذ الفرعية الموجودة في نافذة خصائص المجلد، ويضاف إليها نافذة أخرى هي "افتح باستخدام" تعرضها كما تعرضها لقطة الشاشة السابقة. وكما هو واضح، سيفتح التطبيق المبدئي هذا الملف لعرض محتواه؛ كما يقدم النظام عدة تطبيقات مُزكّاة لفتح هذا الملف، أي التطبيقات القادرة على فتحه والتعامل معه؛ وبإمكانك اختيار أيّ من التطبيقات المزكاة ليكون التطبيق المبدئي بالنقر عليه، ثم النقر على زر "اجعله المبدئي". وإن أردت العودة إلى اﻹعدادات الافتراضية، فانقر الزر"صفِّر". قد تظهر في نافذة الخصائص نافذة فرعية جديدة في بعض أنواع الملفات مثل ملفات الصورة لتقدم معلومات إضافية. ملاحظة: يمكنك النقر على الملف بالزر الأيمن للفأرة، ثم اختيار اﻷمر "افتح بتطبيق آخر" لاختيار تطبيق آخر غير التطبيق المبدئي. لمحة عن إنشاء الملفات وفتحها ما يُنشئ الملفات هي التطبيقات، لهذا عليك أولًا معرفة ما تريد عمله بمساعدة حاسوبك، ثم الاطلاع على التطبيقات التي تتيح لك تنفيذ ما تريده (أو برمجتها إن كنت محبًا للمغامرة والتحدي ومستعدًا لخسارة الشعر أحيانًا!). سنفترض حاليًا أن لديك خلفيةً محدودةً في مبادئ عمل الحاسوب، كما سنفترض أيضًا أنك تريد ببساطة كتابة بعض الملاحظات وبعض التقارير أو إجراء بعض الحسابات وتصفح اﻹنترنت. سنَدُلّك هنا على بعض التطبيقات المثبتة افتراضيًا مع أوبونتو والتي قد تفيدك: LibreOffice Writer: وهو محرر نصوص يمتلك الكثير من الميزات المتقدمة. LiberOffice Calc: تطبيق جداول إلكترونية بميزات متقدمة. Libreoffice Impress: تطبيق إنشاء عروض تقديمية كامل الميزات. LibreOffice Draw: تطبيق ﻹنشاء الرسوميات مع الكثير من الميزات الرائعة. متصفح الويب فايرفوكس: متصفح الويب الغني عن التعريف بكامل إمكاناته. محرر النصوص: تطبيق بسيط لكتابة الملاحظات والنصوص، لكنه شديد اﻷهمية، وخاصةً إن فكرت في البدء بتعلم البرمجة أو تصميم صفحات الويب. تتواجد هذه التطبيقات في قائمة التطبيقات المثبتة، ويمكن فتحها بالنقر على أي منها بالزر الأيسر للفأرة. سيخبرك النظام أن التطبيق جاهز للعمل عندما ينهي تحميله. تطبيق عملي: افتح برنامج "محرر النصوص" ونفذ مايلي: اكتب العبارة "مرحبًا أوبونتو" وانقر الزر "احفظ"، ثم احفظه باسم "test-txt" في مجلد مناسب. احذف اﻵن العبارة السابقة وانسخ والصق ما يلي: <html> <body> <h1> مرحبًا أوبونتو </h1> </body> انقر على زر القائمة بجانب زر "احفظ"، واختر "احفظ باسم"، ثم اجعل اسم هذه النسخة "test-web"، وضعها في نفس المجلد السابق. ستكون النتيجة ظهور الملفين التاليين في المجلد الذي اخترته: لماذا ظهرت أيقونتي الملفين بشكلين مختلفين علمًا أننا استخدمنا نفس البرنامج في إنشائهما؟ الجواب هو: لأن نظام لينكس يقرأ ويفهم محتوى هذا الملف بغض النظر عن التطبيق الذي أنشأه، ومن ثم يبحث عن التطبيق اﻷنسب لتشغيله. إن الملف "test-txt" هو ملف نصي بحت وسيُسند النظام مهمة فتحه إلى محرر النصوص نفسه، لكن اﻵخر يحتوي على معلومات تُدعى شيفرة HTML التي تُستخدم في بناء صفحات ويب، لذلك يسند النظام مهمة فتحه إلى متصفح الويب. جرب بنفسك! تثبيت وإزالة وسائط التخزين كأي نظام تشغيل آخر، تستطيع إضافة وسائط تخزين داخلية أو خارجية إلى جهازك، والعمل معها بكل سهولة ويسر، ماعدا بعض الاستثناءات التقنية البسيطة. سواءً أردت تثبيت قرص صلب خارجي أو بطاقة ذاكرة أو قرص DVD، سيستشعر النظام ما وصلته إلى الحاسب، ويحدد نوعه وينفَّذ ما يُسمى تثبيت للقرص mounting تلقائيًا، ويصبح جاهزًا للاستخدام عندما تظهر أيقونته ضمن شريط التطبيقات. يظهر في لقطة الشاشة السابقة اكتمال تثبيت ذاكرة عبر مدخل USB وقرص مدمج؛ حيث أنه عند النقر على أيقونة أي منهماـ سيفتح النظام هذا الوسيط باستخدام تطبيق "الملفات"، وستكون قادرًا على نقل البيانات منه وإليه. تجدر اﻹشارة أن سواقات اﻷقراص المدمجة لا تُعرض تلقائيًا في شريط التطبيقات أو في أية أماكن أخرى ما لم تضع فيها القرص المدمج؛ وكذلك اﻷمر بالنسبة إلى أقسام القرص الصلب الداخلي أو اﻷقراص الصلبة الداخلية اﻷخرى. فلكي تصل إليها، افتح تطبيق "ملفات"، وانتقل إلى الخيار" أماكن أخرى" أسفل الشريط الجانبي؛ ومن ثم انقر على هذه اﻷقراص نقرًا مزودجًا، وعندها تُثبَّت للاستخدام من قبل مستثمر النظام وتُعرض في شريط التطبيقات. اقرأ أيضًا المقال السابق: تعرّف على سطح مكتب أوبونتو 20.04 تغيير اللغة في نظام لينكس أوبنتو إلى العربية في ماذا يختلف Ubuntu عن Debian؟
×
×
  • أضف...