اختار المؤلف “جمعية مهندسات النساء” لتلقي تبرع كجزء من برنامج “اكتب من أجل التبرعات”.
مقدمة
تحتوي تطبيقات الويب على “دورات الطلب / الاستجابة”. عندما تزور عنوان URL ، يرسل المتصفح طلبًا إلى الخادم الذي يعمل على تطبيق يقوم بمعالجة البيانات أو يقوم بتشغيل استعلامات في قاعدة البيانات. خلال هذه العملية ، يتم الاحتفاظ بالمستخدم في انتظار حتى يعود التطبيق بالاستجابة. بالنسبة لبعض المهام ، يمكن للمستخدم الحصول على استجابة بسرعة ؛ أما بالنسبة للمهام المستغرقة للوقت ، مثل معالجة الصور ، أو تحليل البيانات ، أو إنشاء التقارير ، أو إرسال البريد الإلكتروني ، فإن هذه المهام تستغرق وقتًا طويلاً للانتهاء ويمكن أن تبطئ دورة الطلب / الاستجابة. على سبيل المثال ، فلنفترض أن لديك تطبيقًا يسمح للمستخدمين بتحميل الصور. في هذه الحالة ، قد تحتاج إلى تغيير حجم الصورة أو ضغطها أو تحويلها إلى تنسيق آخر للحفاظ على مساحة القرص الخاصة بالخادم قبل عرض الصورة للمستخدم. معالجة الصورة هي مهمة مكثفة لوحدة المعالجة المركزية (CPU) ، والتي يمكن أن تحجب خيط “Node.js” حتى يتم الانتهاء من المهمة. قد يستغرق ذلك بضع ثوانٍ أو دقائق. يتعين على المستخدمين الانتظار حتى يتم الانتهاء من المهمة للحصول على استجابة من الخادم.
لتجنب بطء دورة الطلب/الاستجابة ، يمكنك استخدام bullmq
، وهو قائمة مهام (وظائف) موزعة تتيح لك تكديس المهام المستغرقة للوقت من تطبيق Node.js الخاص بك إلى bullmq
، مما يفرغ دورة الطلب/الاستجابة. يتيح لك هذا الأداة إرسال استجابات سريعة إلى المستخدم بينما يقوم bullmq
بتنفيذ المهام بشكل غير متزامن في الخلفية ومستقل عن تطبيقك. لتتبع الوظائف ، يستخدم bullmq
Redis لتخزين وصف مختصر لكل وظيفة في قائمة انتظار. ثم يقوم bullmq
بتنفيذ واستدعاء كل وظيفة في قائمة الانتظار ، وتعليمها عند الانتهاء.
في هذه المقالة ، ستستخدم bullmq
لتحرير المهام المستغرقة للوقت في الخلفية ، مما يمكن التطبيق من الاستجابة بسرعة للمستخدمين. أولاً ، ستقوم بإنشاء تطبيق يحتوي على مهمة مستغرقة للوقت بدون استخدام bullmq
. ثم ، ستستخدم bullmq
لتنفيذ المهمة بشكل غير متزامن. وأخيراً ، ستقوم بتثبيت لوحة مرئية لإدارة مهام bullmq
في قائمة Redis.
المتطلبات الأساسية
لمتابعة هذا البرنامج التعليمي ، ستحتاج إلى ما يلي:
-
تهيئة بيئة تطوير Node.js. لنظام Ubuntu 22.04 ، اتبع البرنامج التعليمي الخاص بنا حول كيفية تثبيت Node.js على Ubuntu 22.04. بالنسبة للأنظمة الأخرى ، انظر كيفية تثبيت Node.js وإنشاء بيئة تطوير محلية.
-
Redis مثبت على نظامك. في Ubuntu 22 ، اتبع الخطوات من 1 إلى 3 في البرنامج التعليمي الخاص بنا حول كيفية تثبيت وتأمين Redis على Ubuntu 22.04. بالنسبة للأنظمة الأخرى ، انظر البرنامج التعليمي الخاص بنا حول كيفية تثبيت وتأمين Redis.
-
الاطلاع على promises و async/await functions ، التي يمكنك تطويرها في برنامجنا التعليمي فهم حلقة الأحداث والتعامل معها و Promises و Async/Await في JavaScript.
-
المعرفة الأساسية حول كيفية استخدام Express. راجع البرنامج التعليمي الخاص بنا حول كيفية البدء مع Node.js و Express.
-
الاطلاع على Embedded JavaScript (EJS). قم بزيارة الدرس التعليمي الخاص بنا حول كيفية استخدام EJS لتنسيق تطبيق Node الخاص بك للحصول على مزيد من التفاصيل.
-
فهم أساسي حول كيفية معالجة الصور باستخدام
sharp
، يمكنك التعلم من خلال درسنا حول كيفية معالجة الصور في Node.js باستخدام Sharp.
الخطوة 1 – إعداد مجلد المشروع
في هذه الخطوة، ستقوم بإنشاء مجلد وتثبيت التبعيات اللازمة لتطبيقك. التطبيق الذي ستقوم ببنائه في هذا البرنامج التعليمي سيسمح للمستخدمين بتحميل صورة يتم معالجتها باستخدام حزمة sharp
. معالجة الصور تستغرق وقتًا طويلاً ويمكن أن تبطئ دورة الطلب/الاستجابة، مما يجعل هذه المهمة مرشحة جيدة لاستخدام bullmq
لتفويضها إلى الخلفية. التقنية التي ستستخدمها لتفويض المهمة ستعمل أيضًا لمهام أخرى تستغرق وقتًا طويلاً.
للبدء، قم بإنشاء مجلد يسمى معالج_الصور
وانتقل إلى المجلد:
- mkdir image_processor && cd image_processor
ثم، قم بتهيئة المجلد كحزمة npm:
- npm init -y
ينشئ الأمر ملف package.json
. الخيار -y
يخبر npm بقبول جميع القيم الافتراضية.
عند تشغيل الأمر، ستتطابق النتيجة مع ما يلي:
OutputWrote to /home/sammy/image_processor/package.json:
{
"name": "image_processor",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
تؤكد النتيجة أن ملف package.json
تم إنشاؤه. الخصائص الهامة تشمل اسم التطبيق الخاص بك (name
)، رقم إصدار التطبيق الخاص بك (version
)، ونقطة البدء في مشروعك (main
). إذا كنت ترغب في معرفة المزيد عن الخصائص الأخرى، يمكنك مراجعة وثائق package.json في npm.
التطبيق الذي ستقوم ببنائه في هذا البرنامج التعليمي سيتطلب التبعيات التالية:
express
: إطار عمل ويب لبناء تطبيقات الويب.express-fileupload
: وسيط يسمح لنماذجك بتحميل الملفات.sharp
: مكتبة معالجة الصور.ejs
: لغة قوالب تسمح لك بإنشاء علامات HTML باستخدام Node.js.bullmq
: قائمة مهام موزعة.bull-board
: لوحة تحكم تعتمد علىbullmq
وتعرض حالة المهام بواجهة مستخدم (UI) رائعة.
لتثبيت جميع هذه التبعيات، قم بتشغيل الأمر التالي:
- npm install express express-fileupload sharp ejs bullmq @bull-board/express
بالإضافة إلى التبعيات التي قمت بتثبيتها، ستستخدم أيضًا الصورة التالية في وقت لاحق في هذا البرنامج التعليمي:
استخدم أمر curl
لتنزيل الصورة إلى الموقع الذي تختاره على جهاز الكمبيوتر المحلي الخاص بك
- curl -O https://deved-images.nyc3.cdn.digitaloceanspaces.com/CART-68886/underwater.png
لديك التبعيات اللازمة لبناء تطبيق Node.js الذي لا يحتوي على bullmq
، والذي ستقوم به في الخطوة التالية.
الخطوة 2 – تنفيذ مهمة تستغرق وقتًا دون استخدام bullmq
في هذه الخطوة، ستقوم ببناء تطبيق باستخدام Express يتيح للمستخدمين رفع الصور. سيقوم التطبيق ببدء مهمة تستغرق وقتًا باستخدام sharp
لتغيير حجم الصورة إلى أحجام متعددة، ثم يتم عرضها للمستخدم بعد إرسال الاستجابة. ستساعدك هذه الخطوة على فهم كيفية تأثير المهام التي تستغرق وقتًا على دورة الطلب / الاستجابة.
باستخدام nano
، أو محرر النص الذي تفضله ، قم بإنشاء ملف index.js
:
- nano index.js
في ملف index.js
، قم بإضافة الكود التالي لاستيراد التبعيات:
const path = require("path");
const fs = require("fs");
const express = require("express");
const bodyParser = require("body-parser");
const sharp = require("sharp");
const fileUpload = require("express-fileupload");
في السطر الأول ، قم بإستيراد وحدة path
لحساب مسارات الملفات باستخدام Node. في السطر الثاني ، قم بإستيراد وحدة fs
للتفاعل مع الدلائل. ثم تقوم بإستيراد إطار الويب express
. تقوم بإستيراد وحدة body-parser
لإضافة وسيط لتحليل البيانات في طلبات HTTP. بعد ذلك ، تقوم بإستيراد وحدة sharp
لمعالجة الصور. وأخيرًا ، تقوم بإستيراد express-fileupload
للتعامل مع عمليات التحميل من نموذج HTML.
بعد ذلك ، أضف الكود التالي لتنفيذ وسيط في التطبيق الخاص بك:
...
const app = express();
app.set("view engine", "ejs");
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true,
})
);
أولاً ، قم بتعيين المتغير app
إلى مثيل من Express. ثانياً ، باستخدام المتغير app
، يقوم الأسلوب set()
بتكوين Express لاستخدام لغة القوالب ejs
. ثم تضيف وحدة middleware body-parser
باستخدام الأسلوب use()
لتحويل بيانات JSON في طلبات HTTP إلى متغيرات يمكن الوصول إليها باستخدام JavaScript. في السطر التالي ، تفعل نفس الشيء مع إدخال مشفر بتنسيق URL.
بعد ذلك ، أضف الأسطر التالية لإضافة وحدات middleware إضافية للتعامل مع تحميل الملفات وتقديم الملفات الثابتة:
...
app.use(fileUpload());
app.use(express.static("public"));
تضيف وحدات middleware لتحليل الملفات المحملة عن طريق استدعاء الأسلوب fileUpload()
، وتعيين دليل يبحث فيه Express ويقدم الملفات الثابتة ، مثل الصور و CSS.
بعد ضبط الوحدات الوسيطة ، قم بإنشاء مسار يعرض نموذج HTML لتحميل صورة:
...
app.get("/", function (req, res) {
res.render("form");
});
هنا ، تستخدم أسلوب get()
من وحدة Express لتحديد المسار /
والوظيفة الاستدعاء التي يجب تشغيلها عندما يزور المستخدم الصفحة الرئيسية أو المسار /
. في الوظيفة الاستدعاء ، تستدعي res.render()
لتقديم ملف form.ejs
في دليل views
. لم تقم بعد بإنشاء ملف form.ejs
أو دليل views
.
لإنشائه ، أولاً ، احفظ الملف وأغلقه. في الوحدة النمطية الخاصة بك ، أدخل الأمر التالي لإنشاء دليل views
في دليل مشروع الجذر الخاص بك:
- mkdir views
انتقل إلى دليل views
:
- cd views
أنشئ ملف form.ejs
في محرر النص:
- nano form.ejs
في ملف form.ejs
الخاص بك ، أضف الكود التالي لإنشاء النموذج:
<!DOCTYPE html>
<html lang="en">
<%- include('./head'); %>
<body>
<div class="home-wrapper">
<h1>Image Processor</h1>
<p>
Resizes an image to multiple sizes and converts it to a
<a href="https://en.wikipedia.org/wiki/WebP">webp</a> format.
</p>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input
type="file"
name="image"
placeholder="Select image from your computer"
/>
<button type="submit">Upload Image</button>
</form>
</div>
</body>
</html>
أولاً ، تشير إلى ملف head.ejs
الذي لم تقم بإنشائه بعد. سيحتوي ملف head.ejs
على عنصر head
HTML الذي يمكنك الاشارة إليه في صفحات HTML الأخرى.
في علامة body
، قم بإنشاء نموذج بالسمات التالية:
- تحدد
action
المسار الذي يجب إرسال بيانات النموذج إليه عند تقديم النموذج. - تحدد
method
الطريقة المستخدمة في إرسال البيانات. تضمن الطريقةPOST
تضمين البيانات في طلب HTTP. - تحدد
encytype
كيفية ترميز بيانات النموذج. القيمةmultipart/form-data
تمكّن عناصر HTMLinput
من تحميل بيانات الملف.
في عنصر form
، قم بإنشاء علامة input
لتحميل الملفات. ثم قم بتعريف عنصر button
مع سمة type
محددة على submit
، مما يتيح لك تقديم النماذج.
بمجرد الانتهاء ، قم بحفظ وإغلاق الملف.
ثم ، قم بإنشاء ملف head.ejs
:
- nano head.ejs
في ملف head.ejs
الخاص بك ، أضف الكود التالي لإنشاء قسم الرأس في التطبيق:
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Image Processor</title>
<link rel="stylesheet" href="css/main.css" />
</head>
هنا ، تشير إلى ملف main.css
الذي ستقوم بإنشائه في الدليل public
لاحقًا في هذه الخطوة. سيحتوي ذلك الملف على الأنماط الخاصة بهذا التطبيق. في الوقت الحالي ، ستستمر في إعداد العمليات للأصول الثابتة.
قم بحفظ وإغلاق الملف.
للتعامل مع البيانات المرسلة من النموذج ، يجب عليك تعريف طريقة post
في Express. للقيام بذلك ، عد إلى الدليل الجذر لمشروعك:
- cd ..
افتح ملف index.js
مرة أخرى:
- nano index.js
في ملف index.js
الخاص بك، قم بإضافة الأسطر المميزة لتعريف طريقة لمعالجة إرسال النموذج على المسار /upload
:
app.get("/", function (req, res) {
...
});
app.post("/upload", async function (req, res) {
const { image } = req.files;
if (!image) return res.sendStatus(400);
});
تستخدم المتغير app
لاستدعاء طريقة post()
، والتي ستتعامل مع النموذج المرسل على المسار /upload
. بعد ذلك، تستخرج بيانات الصورة المرفوعة من طلب HTTP إلى المتغير image
. بعد ذلك، تقوم بتعيين استجابة لإرجاع رمز الحالة 400
إذا لم يقم المستخدم بتحميل صورة.
لتعيين عملية للصورة المحمّلة، أضف الكود المميز التالي:
...
app.post("/upload", async function (req, res) {
const { image } = req.files;
if (!image) return res.sendStatus(400);
const imageName = path.parse(image.name).name;
const processImage = (size) =>
sharp(image.data)
.resize(size, size)
.webp({ lossless: true })
.toFile(`./public/images/${imageName}-${size}.webp`);
sizes = [90, 96, 120, 144, 160, 180, 240, 288, 360, 480, 720, 1440];
Promise.all(sizes.map(processImage));
});
تمثل هذه الأسطر كيفية معالجة التطبيق الخاص بك للصورة. أولاً، تقوم بإزالة امتداد الصورة من الصورة المحمّلة وتحفظ الاسم في المتغير imageName
. بعد ذلك، تعرف وظيفة processImage()
. تأخذ هذه الوظيفة المعامل size
، حيث سيتم استخدام قيمتها لتحديد أبعاد الصورة أثناء التغيير في الحجم. في الوظيفة، تستدعي sharp()
باستخدام image.data
، وهو buffer يحتوي على البيانات الثنائية للصورة المحمّلة. يعيد sharp
تغيير حجم الصورة وفقًا للقيمة الموجودة في معامل الحجم. تستخدم طريقة webp()
من sharp
لتحويل الصورة إلى تنسيق صورة webp. بعد ذلك، تحفظ الصورة في دليل public/images/
.
القائمة التالية من الأرقام تحدد الأحجام التي ستستخدم لتغيير حجم الصورة المُرفَقة. بعد ذلك ، استخدم طريقة map()
في JavaScript لاستدعاء processImage()
لكل عنصر في مصفوفة sizes
، بعد ذلك ستقوم بإرجاع مصفوفة جديدة. في كل مرة يستدعي فيها طريقة map()
دالة processImage()
، ستقوم بإرجاع وعد إلى المصفوفة الجديدة. تستخدم طريقة Promise.all()
لحلها.
تختلف سرعات معالجة الحاسوب ، وكذلك حجم الصور التي يمكن للمستخدم تحميلها ، وهو ما قد يؤثر على سرعة معالجة الصورة. لتأخير هذا الكود لأغراض التوضيح ، قم بإدراج الأسطر المظللة لإضافة حلقة زيادة شديدة لوحدة المعالجة المركزية وإعادة توجيهها إلى صفحة ستعرض الصور المصغرة بالأسطر المظللة:
...
app.post("/upload", async function (req, res) {
...
let counter = 0;
for (let i = 0; i < 10_000_000_000; i++) {
counter++;
}
res.redirect("/result");
});
سوف تعمل الحلقة 10 مليار مرة لزيادة متغير counter
. يتم استدعاء الدالة res.redirect()
لإعادة توجيه التطبيق إلى المسار /result
. سيقوم المسار بعرض صفحة HTML ستعرض الصور في الدليل public/images
.
المسار /result
لا يوجد بعد. لإنشائه ، أضف الكود المظلل في ملف index.js
الخاص بك:
...
app.get("/", function (req, res) {
...
});
app.get("/result", (req, res) => {
const imgDirPath = path.join(__dirname, "./public/images");
let imgFiles = fs.readdirSync(imgDirPath).map((image) => {
return `images/${image}`;
});
res.render("result", { imgFiles });
});
app.post("/upload", async function (req, res) {
...
});
تعرف المسار /result
باستخدام الطريقة app.get()
. في الدالة ، تعرف متغير imgDirPath
بالمسار الكامل إلى دليل public/images
. تستخدم طريقة readdirSync()
في وحدة fs
لقراءة جميع الملفات في الدليل المحدد. من هناك ، تقوم بسلسلة من طرق map()
لإرجاع مصفوفة جديدة تحتوي على مسارات الصور مع بادئة images/
.
أخيرًا ، تستدعي res.render()
لتقديم ملف result.ejs
، الذي لا يوجد حتى الآن. تمرر المتغير imgFiles
، الذي يحتوي على مصفوفة بجميع مسارات الصور النسبية ، إلى ملف result.ejs
.
احفظ وأغلق الملف.
لإنشاء ملف result.ejs
، عد إلى دليل views
:
- cd views
أنشئ وافتح ملف result.ejs
في محررك:
- nano result.ejs
في ملف result.ejs
الخاص بك ، أضف الأسطر التالية لعرض الصور:
<!DOCTYPE html>
<html lang="en">
<%- include('./head'); %>
<body>
<div class="gallery-wrapper">
<% if (imgFiles.length > 0){%>
<p>The following are the processed images:</p>
<ul>
<% for (let imgFile of imgFiles){ %>
<li><img src=<%= imgFile %> /></li>
<% } %>
</ul>
<% } else{ %>
<p>
The image is being processed. Refresh after a few seconds to view the
resized images.
</p>
<% } %>
</div>
</body>
</html>
أولاً ، تشير إلى ملف head.ejs
. في علامة body
، تحقق مما إذا كان المتغير imgFiles
فارغًا. إذا كان يحتوي على بيانات ، فتكرر عبر كل ملف وأنشئ صورة لكل عنصر في المصفوفة. إذا كان imgFiles
فارغًا ، فاطبع رسالة تخبر المستخدم بـ إعادة التحميل بعد بضع ثوان لعرض الصور المصغرة.
.
احفظ وأغلق الملف.
بعد ذلك ، عد إلى الدليل الرئيسي وأنشئ الدليل public
الذي سيحتوي على الأصول الثابتة الخاصة بك:
- cd .. && mkdir public
انتقل إلى المجلد العام:
- cd public
أنشئ مجلدًا بعنوان “images” لتخزين الصور المرفوعة:
- mkdir images
بعد ذلك، قم بإنشاء مجلد بعنوان “css” وانتقل إليه:
- mkdir css && cd css
في محرر النصوص الخاص بك، أنشئ وافتح ملف “main.css” الذي تم الإشارة إليه سابقًا في ملف “head.ejs”:
- nano main.css
في ملف “main.css” الخاص بك، أضف الأنماط التالية:
body {
background: #f8f8f8;
}
h1 {
text-align: center;
}
p {
margin-bottom: 20px;
}
a:link,
a:visited {
color: #00bcd4;
}
/** أنماط لزر "اختر الملف" **/
button[type="submit"] {
background: none;
border: 1px solid orange;
padding: 10px 30px;
border-radius: 30px;
transition: all 1s;
}
button[type="submit"]:hover {
background: orange;
}
/** أنماط لزر "رفع الصورة" **/
input[type="file"]::file-selector-button {
border: 2px solid #2196f3;
padding: 10px 20px;
border-radius: 0.2em;
background-color: #2196f3;
}
ul {
list-style: none;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.home-wrapper {
max-width: 500px;
margin: 0 auto;
padding-top: 100px;
}
.gallery-wrapper {
max-width: 1200px;
margin: 0 auto;
}
ستقوم هذه الأسطر بتنسيق عناصر التطبيق. باستخدام سمات HTML، ستقوم بتنسيق خلفية زر “اختر الملف” باستخدام رمز الهيكس “#2196f3” (ظلال الأزرق) وحدود زر “رفع الصورة” باللون البرتقالي. ستقوم أيضًا بتنسيق العناصر في مسار “/result” لجعلها أكثر جاذبية.
بمجرد الانتهاء، قم بحفظ الملف وإغلاقه.
عد إلى مجلد الجذر الخاص بالمشروع:
- cd ../..
افتح ملف “index.js” في محرر النصوص الخاص بك:
- nano index.js
في ملف “index.js” الخاص بك، أضف الكود التالي الذي سيبدأ تشغيل الخادم:
...
app.listen(3000, function () {
console.log("Server running on port 3000");
});
الملف الكامل “index.js” سيكون كالتالي:
const path = require("path");
const fs = require("fs");
const express = require("express");
const bodyParser = require("body-parser");
const sharp = require("sharp");
const fileUpload = require("express-fileupload");
const app = express();
app.set("view engine", "ejs");
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true,
})
);
app.use(fileUpload());
app.use(express.static("public"));
app.get("/", function (req, res) {
res.render("form");
});
app.get("/result", (req, res) => {
const imgDirPath = path.join(__dirname, "./public/images");
let imgFiles = fs.readdirSync(imgDirPath).map((image) => {
return `images/${image}`;
});
res.render("result", { imgFiles });
});
app.post("/upload", async function (req, res) {
const { image } = req.files;
if (!image) return res.sendStatus(400);
const imageName = path.parse(image.name).name;
const processImage = (size) =>
sharp(image.data)
.resize(size, size)
.webp({ lossless: true })
.toFile(`./public/images/${imageName}-${size}.webp`);
sizes = [90, 96, 120, 144, 160, 180, 240, 288, 360, 480, 720, 1440];
Promise.all(sizes.map(processImage));
let counter = 0;
for (let i = 0; i < 10_000_000_000; i++) {
counter++;
}
res.redirect("/result");
});
app.listen(3000, function () {
console.log("Server running on port 3000");
});
بمجرد الانتهاء من إجراء التغييرات، قم بحفظ الملف وإغلاقه.
قم بتشغيل التطبيق باستخدام أمر “node”:
- node index.js
ستحصل على نتيجة مشابهة للتالية:
OutputServer running on port 3000
تؤكد هذه النتيجة أن الخادم يعمل بدون أي مشاكل.
افتح المتصفح الذي تفضله وقم بزيارة http://localhost:3000/
.
ملاحظة: إذا كنت تتابع البرنامج التعليمي على خادم عن بُعد، يمكنك الوصول إلى التطبيق في المتصفح المحلي باستخدام إعادة توجيه المنفذ.
بينما يعمل خادم Node.js، افتح نافذة الأوامر الأخرى وأدخل الأمر التالي:
- ssh -L 3000:localhost:3000 your-non-root-user@yourserver-ip
بمجرد الاتصال بالخادم، قم بتشغيل node index.js
ثم انتقل إلى http://localhost:3000/
في متصفح الويب الخاص بجهازك المحلي.
عندما يتم تحميل الصفحة، ستتطابق مع ما يلي:
بعد ذلك، اضغط على زر اختر ملف وحدد صورة underwater.png
على جهازك المحلي. ستتغير العرض من No file chosen إلى underwater.png. بعد ذلك، اضغط على زر تحميل الصورة. سيستغرق التطبيق بعض الوقت لمعالجة الصورة وتشغيل الحلقة المتزايدة.
بمجرد انتهاء المهمة، ستتم تحميل مسار /result
مع الصور المصغرة:
يمكنك الآن إيقاف الخادم باستخدام CTRL+C
. لا يقوم Node.js بإعادة تحميل الخادم تلقائيًا عند تغيير الملفات، لذا ستحتاج إلى إيقاف وإعادة تشغيل الخادم كلما قمت بتحديث الملفات.
الآن تعرف كيف يمكن لمهمة تستغرق وقتًا طويلاً أن تؤثر على دورة طلب/استجابة التطبيق. ستقوم بتنفيذ المهمة بشكل غير متزامن بعد ذلك.
الخطوة 3 — تنفيذ المهام المكلفة بالوقت بشكل غير متزامن باستخدام bullmq
في هذه الخطوة، ستقوم بتفويض مهمة تستغرق وقتًا طويلاً إلى الخلفية باستخدام bullmq
. ستتيح هذه التعديلات تحرير دورة الطلب/الاستجابة وتسمح لتطبيقك بالاستجابة للمستخدمين فوراً بينما يتم معالجة الصورة.
للقيام بذلك، تحتاج إلى إنشاء وصف موجز للمهمة وإضافتها إلى طابور الانتظار باستخدام bullmq
. طابور الانتظار هو هيكل بيانات يعمل بنفس طريقة عمل طابور الانتظار في الحياة الواقعية. عندما يصطف الناس للدخول إلى مكان، يكون الشخص الأول في الصف هو الشخص الأول الذي يدخل المكان. أي شخص يأتي بعده سيصطف في نهاية الصف وسيدخل المكان بعد كل من سبقه في الصف حتى يدخل الشخص الأخير المكان. بعملية الإضافة والإزالة (dequeue) حسب ترتيب الوصول الأول (الدخول الأول، الخروج الأول) في هيكل بيانات طابور الانتظار، يتم إزالة العنصر الأول المضاف إلى الطابور أولاً. باستخدام bullmq
، سيقوم المنتج بإضافة مهمة في طابور الانتظار، وسيقوم المستهلك (أو العامل) بإزالة المهمة من طابور الانتظار وتنفيذها.
الطابور في bullmq
يتم تخزينه في Redis. عندما تصف وتضيف وظيفة إلى الطابور ، يتم إنشاء إدخال للوظيفة في طابور Redis. يمكن أن تكون وصف الوظيفة سلسلة أو كائن مع خصائص تحتوي على بيانات أدنى أو مراجع إلى البيانات التي ستسمح لـ bullmq
بتنفيذ الوظيفة لاحقًا. بمجرد تحديد الوظيفة لإضافة الوظائف إلى الطابور ، تقوم بنقل الكود الذي يستغرق وقتًا طويلاً إلى وظيفة منفصلة. في وقت لاحق ، ستقوم bullmq
باستدعاء هذه الوظيفة باستخدام البيانات التي قمت بتخزينها في الطابور عندما يتم استخراج الوظيفة من الطابور. بمجرد الانتهاء من المهمة ، سيقوم bullmq
بوضع علامة على أنها تمت ، وسحب وظيفة أخرى من الطابور وتنفيذها.
افتح index.js
في محرر النصوص الخاص بك:
- nano index.js
في ملف index.js
الخاص بك ، قم بإضافة الأسطر المظللة لإنشاء طابور في Redis باستخدام bullmq
:
...
const fileUpload = require("express-fileupload");
const { Queue } = require("bullmq");
const redisOptions = { host: "localhost", port: 6379 };
const imageJobQueue = new Queue("imageJobQueue", {
connection: redisOptions,
});
async function addJob(job) {
await imageJobQueue.add(job.type, job);
}
...
تبدأ باستخراج فئة Queue
من bullmq
، والتي تُستخدم لإنشاء طابور في Redis. ثم تقوم بتعيين متغير redisOptions
إلى كائن يحتوي على خصائص يستخدمها مثيل فئة Queue
لإنشاء اتصال مع Redis. تعيين قيمة خاصية host
إلى localhost
لأن Redis يعمل على جهازك المحلي.
ملاحظة: إذا كان Redis يعمل على خادم بعيد مستقل عن تطبيقك ، فيجب تحديث قيمة خاصية host
إلى عنوان IP للخادم البعيد. كما تعيين قيمة خاصية port
إلى 6379
، وهو منفذ افتراضي يستخدمه Redis للاستماع للاتصالات.
إذا قمت بإعداد توجيه المنفذ إلى خادم بعيد يعمل Redis والتطبيق معًا ، فلن تحتاج إلى تحديث الخاصية “host” ، ولكن ستحتاج إلى استخدام اتصال توجيه المنفذ في كل مرة تقوم فيها بتسجيل الدخول إلى الخادم لتشغيل التطبيق.
ثم ، قم بتعيين متغير “imageJobQueue” إلى مثيل من فئة “Queue” ، مع أخذ اسم الطابور كوسيطة أولى وكائن كوسيطة ثانية. يحتوي الكائن على خاصية “connection” بقيمة محددة على أساس كائن في المتغير “redisOptions”. بعد إنشاء فئة “Queue” ، سيتم إنشاء طابور يسمى “imageJobQueue” في Redis.
أخيرًا ، قم بتعريف وظيفة “addJob()” التي ستستخدمها لإضافة وظيفة في “imageJobQueue”. تأخذ الوظيفة معلمة “job” تحتوي على معلومات حول الوظيفة (ستقوم باستدعاء وظيفة “addJob()” بالبيانات التي ترغب في حفظها في طابور). في الوظيفة ، يتم استدعاء طريقة “add()” في “imageJobQueue” ، ويتم أخذ اسم الوظيفة كمعامل أول وبيانات الوظيفة كمعامل ثانٍ.
أضف الكود المميز لاستدعاء وظيفة “addJob()” لإضافة وظيفة في الطابور:
...
app.post("/upload", async function (req, res) {
const { image } = req.files;
if (!image) return res.sendStatus(400);
const imageName = path.parse(image.name).name;
...
await addJob({
type: "processUploadedImages",
image: {
data: image.data.toString("base64"),
name: image.name,
},
});
res.redirect("/result");
});
...
هنا، تقوم بإستدعاء الدالة addJob()
مع كائن يصف الوظيفة. الكائن يحتوي على سمة type
بقيمة اسم الوظيفة. الخاصية الثانية image
تحتوي على كائن يحتوي على بيانات الصورة التي قام المستخدم بتحميلها. نظرًا لأن بيانات الصورة في image.data
بصيغة مخبأة (ثنائية)، يتم استدعاء طريقة JavaScript toString()
لتحويلها إلى سلسلة نصية يمكن تخزينها في Redis، مما سيضبط الخاصية data
كنتيجة. يتم تعيين خاصية image
إلى اسم الصورة المحملة (بما في ذلك امتداد الصورة).
لقد قمت الآن بتعريف المعلومات المطلوبة لتنفيذ هذه الوظيفة في bullmq
لاحقًا. اعتمادًا على وظيفتك، قد تقوم بإضافة مزيد من المعلومات أو أقل.
تحذير: نظرًا لأن Redis قاعدة بيانات في الذاكرة، تجنب تخزين كميات كبيرة من البيانات للوظائف في قائمة الانتظار. إذا كان لديك ملف كبير يحتاج الوظيفة لمعالجته، قم بحفظ الملف على القرص أو السحابة، ثم حفظ رابط الملف كسلسلة نصية في قائمة الانتظار. عندما ينفذ bullmq
الوظيفة، سيقوم بجلب الملف من الرابط المحفوظ في Redis.
احفظ وأغلق الملف الخاص بك.
في الخطوة التالية، قم بإنشاء وفتح ملف utils.js
الذي سيحتوي على كود معالجة الصورة:
- nano utils.js
في ملف utils.js
الخاص بك، أضف الكود التالي لتعريف الدالة المسؤولة عن معالجة الصورة:
const path = require("path");
const sharp = require("sharp");
function processUploadedImages(job) {
}
module.exports = { processUploadedImages };
تقوم بإستيراد الوحدات اللازمة لمعالجة الصور وحساب المسارات في أول سطرين. ثم تقوم بتعريف الدالة processUploadedImages()
، والتي ستحتوي على مهمة معالجة الصور المستغرقة للوقت. تأخذ هذه الدالة معامل job
الذي سيتم ملؤه عندما يقوم العامل بجلب بيانات المهمة من الطابور ثم استدعاء الدالة processUploadedImages()
مع بيانات الطابور. كما تقوم بتصدير الدالة processUploadedImages()
حتى تتمكن من الاشارة إليها في ملفات أخرى.
احفظ الملف وأغلقه.
عد إلى ملف index.js
:
- nano index.js
انسخ الأسطر المظللة من ملف index.js
، ثم احذفها من هذا الملف. ستحتاج إلى الكود المنسوخ لاحقًا، لذا احفظه في الحافظة. إذا كنت تستخدم nano
، يمكنك تظليل هذه الأسطر والنقر بالزر الأيمن للفأرة ثم نسخها:
...
app.post("/upload", async function (req, res) {
const { image } = req.files;
if (!image) return res.sendStatus(400);
const imageName = path.parse(image.name).name;
const processImage = (size) =>
sharp(image.data)
.resize(size, size)
.webp({ lossless: true })
.toFile(`./public/images/${imageName}-${size}.webp`);
sizes = [90, 96, 120, 144, 160, 180, 240, 288, 360, 480, 720, 1440];
Promise.all(sizes.map(processImage))
let counter = 0;
for (let i = 0; i < 10_000_000_000; i++) {
counter++;
};
...
res.redirect("/result");
});
الآن، سيتم تطابق طريقة post
لمسار upload
كما يلي:
...
app.post("/upload", async function (req, res) {
const { image } = req.files;
if (!image) return res.sendStatus(400);
await addJob({
type: "processUploadedImages",
image: {
data: image.data.toString("base64"),
name: image.name,
},
});
res.redirect("/result");
});
...
احفظ وأغلق هذا الملف، ثم افتح ملف utils.js
:
- nano utils.js
في ملف utils.js
الخاص بك، الصق الأسطر التي قمت بنسخها لمعالجة طريقة استدعاء مسار /upload
في دالة processUploadedImages
:
...
function processUploadedImages(job) {
const imageName = path.parse(image.name).name;
const processImage = (size) =>
sharp(image.data)
.resize(size, size)
.webp({ lossless: true })
.toFile(`./public/images/${imageName}-${size}.webp`);
sizes = [90, 96, 120, 144, 160, 180, 240, 288, 360, 480, 720, 1440];
Promise.all(sizes.map(processImage));
let counter = 0;
for (let i = 0; i < 10_000_000_000; i++) {
counter++;
};
}
...
الآن بعد أن قمت بنقل الكود الخاص بمعالجة الصورة، يتعين عليك تحديثه لاستخدام بيانات الصورة من معامل job
لدالة processUploadedImages()
التي قمت بتعريفها سابقًا.
للقيام بذلك، أضف وحدث الأسطر المظللة التالية:
function processUploadedImages(job) {
const imageFileData = Buffer.from(job.image.data, "base64");
const imageName = path.parse(job.image.name).name;
const processImage = (size) =>
sharp(imageFileData)
.resize(size, size)
.webp({ lossless: true })
.toFile(`./public/images/${imageName}-${size}.webp`);
...
}
تقوم بتحويل النسخة المسلسلة لبيانات الصورة إلى بيانات ثنائية باستخدام الطريقة Buffer.from()
. ثم تقوم بتحديث path.parse()
بالإشارة إلى اسم الصورة المحفوظة في قائمة الانتظار. بعد ذلك، تقوم بتحديث الطريقة sharp()
لتأخذ بيانات الصورة الثنائية المخزنة في المتغير imageFileData
.
يكون محتوى ملف utils.js
الكامل كما يلي:
const path = require("path");
const sharp = require("sharp");
function processUploadedImages(job) {
const imageFileData = Buffer.from(job.image.data, "base64");
const imageName = path.parse(job.image.name).name;
const processImage = (size) =>
sharp(imageFileData)
.resize(size, size)
.webp({ lossless: true })
.toFile(`./public/images/${imageName}-${size}.webp`);
sizes = [90, 96, 120, 144, 160, 180, 240, 288, 360, 480, 720, 1440];
Promise.all(sizes.map(processImage));
let counter = 0;
for (let i = 0; i < 10_000_000_000; i++) {
counter++;
};
}
module.exports = { processUploadedImages };
احفظ وأغلق الملف الخاص بك، ثم اعد إلى ملف index.js
:
- nano index.js
لا يعد المتغير sharp
مطلوبًا كتبعية حيث يتم معالجة الصورة الآن في ملف utils.js
. احذف السطر المشار إليه من الملف:
const bodyParser = require("body-parser");
const sharp = require("sharp");
const fileUpload = require("express-fileupload");
const { Queue } = require("bullmq");
...
احفظ وأغلق الملف الخاص بك.
لقد قمت الآن بتحديد وظيفة إنشاء قائمة في Redis وإضافة وظيفة. لقد قمت أيضًا بتحديد وظيفة processUploadedImages()
لمعالجة الصور المحملة.
المهمة المتبقية هي إنشاء مستهلك (أو عامل) الذي سيقوم بسحب وظيفة من قائمة الانتظار واستدعاء وظيفة processUploadedImages()
مع بيانات الوظيفة.
أنشئ ملف worker.js
في محرر النصوص:
- nano worker.js
في ملف worker.js
الخاص بك، أضف الكود التالي:
const { Worker } = require("bullmq");
const { processUploadedImages } = require("./utils");
const workerHandler = (job) => {
console.log("Starting job:", job.name);
processUploadedImages(job.data);
console.log("Finished job:", job.name);
return;
};
في السطر الأول، قم بإستيراد فئة Worker
من bullmq
؛ عند تهيئتها، ستبدأ العامل بإخراج الوظائف من قائمة الانتظار في Redis وتنفيذها. بعد ذلك، قم بالإشارة إلى الوظيفة processUploadedImages()
من ملف utils.js
حتى يتمكن العامل من استدعاء الوظيفة بالبيانات الموجودة في قائمة الانتظار.
تعرف دالة workerHandler()
التي تأخذ معامل job
يحتوي على بيانات المهمة في الطابور. في الدالة، تسجل أن المهمة قد بدأت، ثم تقوم بإستدعاء processUploadedImages()
باستخدام بيانات المهمة. بعد ذلك، تسجل رسالة نجاح وتعيد null
.
للسماح للعامل بالاتصال بـ Redis، وإخراج مهمة من الطابور، واستدعاء workerHandler()
ببيانات المهمة، أضف الأسطر التالية إلى الملف:
...
const workerOptions = {
connection: {
host: "localhost",
port: 6379,
},
};
const worker = new Worker("imageJobQueue", workerHandler, workerOptions);
console.log("Worker started!");
هنا، قم بتعيين المتغير workerOptions
إلى كائن يحتوي على إعدادات اتصال Redis. قم بتعيين المتغير worker
إلى مثيل من فئة Worker
التي تأخذ المعاملات التالية:
imageJobQueue
: اسم طابور المهام.workerHandler
: الدالة التي ستعمل بعد استخراج مهمة من طابور Redis.workerOptions
: إعدادات تكوين Redis التي يستخدمها العامل لإنشاء اتصال مع Redis.
أخيرًا، سجل رسالة نجاح.
بعد إضافة الأسطر، قم بحفظ وإغلاق الملف.
لقد قمت الآن بتعريف وظيفة العامل bullmq
لاستخراج المهام من الطابور وتنفيذها.
في الوحدة النمطية الخاصة بك، قم بحذف الصور في الدليل public/images
حتى تتمكن من البدء من جديد في اختبار تطبيقك:
- rm public/images/*
بعد ذلك، قم بتشغيل ملف index.js
:
- node index.js
سيبدأ التطبيق:
OutputServer running on port 3000
الآن ستبدأ العامل. افتح جلسة طرفية ثانية وانتقل إلى مجلد المشروع:
- cd image_processor/
ابدأ العامل بالأمر التالي:
- node worker.js
سيبدأ العامل:
OutputWorker started!
قم بزيارة http://localhost:3000/
في متصفحك. اضغط على زر اختر ملف وحدد underwater.png
من جهاز الكمبيوتر الخاص بك، ثم اضغط على زر رفع الصورة.
قد تتلقى استجابة فورية تخبرك بضرورة تحديث الصفحة بعد بضع ثوانٍ:
بدلاً من ذلك، قد تتلقى استجابة فورية تحتوي على بعض الصور المعالجة على الصفحة بينما البعض الآخر لا يزال يتم معالجته:
يمكنك تحديث الصفحة عدة مرات لتحميل جميع الصور المصغرة.
عد إلى الطرفية حيث يعمل العامل. ستحتوي تلك الطرفية على رسالة تطابق ما يلي:
OutputWorker started!
Starting job: processUploadedImages
Finished job: processUploadedImages
يؤكد الإخراج أن bullmq
قام بتشغيل المهمة بنجاح.
يمكن لتطبيقك ما زال أن يقوم بتفويض المهام التي تستغرق وقتًا طويلاً حتى إذا لم يكن العامل قيد التشغيل. لتوضيح ذلك، قم بإيقاف العامل في الطرفية الثانية باستخدام CTRL+C
.
في جلستك الأصلية في الطرفية، قم بإيقاف خادم Express وحذف الصور في public/images
:
- rm public/images/*
بعد ذلك، قم بتشغيل الخادم مرة أخرى:
- node index.js
في المتصفح الخاص بك، قم بزيارة http://localhost:3000/
وقم بتحميل صورة underwater.png
مرة أخرى. عندما يتم إعادة توجيهك إلى المسار /result
، لن تظهر الصور على الصفحة لأن العامل غير قيد التشغيل:
عد إلى الطرفية التي قمت بتشغيل العامل فيها وقم بتشغيل العامل مرة أخرى:
- node worker.js
سيتطابق الإخراج مع ما يلي، مما يخبرك بأن المهمة قد بدأت:
OutputWorker started!
Starting job: processUploadedImages
بعد إكمال العمل وتشمل النتيجة سطرًا يقرأ Finished job: processUploadedImages
، قم بتحديث المتصفح. ستتم تحميل الصور الآن:
قم بإيقاف الخادم والعامل.
يمكنك الآن تفويض مهمة تستغرق وقتًا طويلاً للخلفية وتنفيذها بشكل غير متزامن باستخدام bullmq
. في الخطوة التالية، ستقوم بإعداد لوحة لمراقبة حالة الطابور.
الخطوة 4 – إضافة لوحة لمراقبة طوابير bullmq
في هذه الخطوة، ستستخدم حزمة bull-board
لمراقبة المهام في طابور Redis من لوحة بصرية. ستقوم هذه الحزمة تلقائيًا بإنشاء لوحة تحكم واجهة مستخدم (UI) تعرض وتنظم المعلومات حول المهام bullmq
المخزنة في طابور Redis. باستخدام متصفحك، يمكنك مراقبة المهام التي تم إكمالها، أو في انتظار التنفيذ، أو فشلت دون الحاجة إلى فتح واجهة سطر الأوامر CLI لـ Redis في الترمينال.
افتح ملف index.js
في محرر النصوص الخاص بك:
- nano index.js
أضف الكود المميز المشار إليه لاستيراد bull-board
:
...
const { Queue } = require("bullmq");
const { createBullBoard } = require("@bull-board/api");
const { BullMQAdapter } = require("@bull-board/api/bullMQAdapter");
const { ExpressAdapter } = require("@bull-board/express");
...
في الكود السابق، تستورد الطريقة createBullBoard()
من bull-board
. كما تستورد BullMQAdapter
، الذي يسمح لـ bull-board
الوصول إلى طوابير bullmq
، و ExpressAdapter
، الذي يوفر وظائف لـ Express لعرض لوحة التحكم.
بعد ذلك، أضف الكود المميز أدناه لربط bull-board
مع bullmq
:
...
async function addJob(job) {
...
}
const serverAdapter = new ExpressAdapter();
const bullBoard = createBullBoard({
queues: [new BullMQAdapter(imageJobQueue)],
serverAdapter: serverAdapter,
});
serverAdapter.setBasePath("/admin");
const app = express();
...
أولاً، قم بتعيين serverAdapter
إلى مثيل من ExpressAdapter
. ثم، استدعِ createBullBoard()
لتهيئة لوحة التحكم ببيانات طابور bullmq
. قم بتمرير كائن يحتوي على خاصيتي queues
و serverAdapter
كوسيطات. الخاصية الأولى، queues
، تقبل مصفوفة من الطوابير التي قمت بتعريفها باستخدام bullmq
، وهنا هو imageJobQueue
. الخاصية الثانية، serverAdapter
، تحتوي على كائن يقبل مثيلًا من محول خادم Express. بعد ذلك، قم بتعيين المسار /admin
للوصول إلى لوحة التحكم باستخدام طريقة setBasePath()
.
بعد ذلك، أضف وسيط serverAdapter
لمسار /admin
:
app.use(express.static("public"))
app.use("/admin", serverAdapter.getRouter());
app.get("/", function (req, res) {
...
});
سيتطابق محتوى ملف index.js
النهائي مع المحتوى التالي:
const path = require("path");
const fs = require("fs");
const express = require("express");
const bodyParser = require("body-parser");
const fileUpload = require("express-fileupload");
const { Queue } = require("bullmq");
const { createBullBoard } = require("@bull-board/api");
const { BullMQAdapter } = require("@bull-board/api/bullMQAdapter");
const { ExpressAdapter } = require("@bull-board/express");
const redisOptions = { host: "localhost", port: 6379 };
const imageJobQueue = new Queue("imageJobQueue", {
connection: redisOptions,
});
async function addJob(job) {
await imageJobQueue.add(job.type, job);
}
const serverAdapter = new ExpressAdapter();
const bullBoard = createBullBoard({
queues: [new BullMQAdapter(imageJobQueue)],
serverAdapter: serverAdapter,
});
serverAdapter.setBasePath("/admin");
const app = express();
app.set("view engine", "ejs");
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true,
})
);
app.use(fileUpload());
app.use(express.static("public"));
app.use("/admin", serverAdapter.getRouter());
app.get("/", function (req, res) {
res.render("form");
});
app.get("/result", (req, res) => {
const imgDirPath = path.join(__dirname, "./public/images");
let imgFiles = fs.readdirSync(imgDirPath).map((image) => {
return `images/${image}`;
});
res.render("result", { imgFiles });
});
app.post("/upload", async function (req, res) {
const { image } = req.files;
if (!image) return res.sendStatus(400);
await addJob({
type: "processUploadedImages",
image: {
data: Buffer.from(image.data).toString("base64"),
name: image.name,
},
});
res.redirect("/result");
});
app.listen(3000, function () {
console.log("Server running on port 3000");
});
بمجرد الانتهاء من إجراء التغييرات، قم بحفظ الملف وإغلاقه.
قم بتشغيل ملف index.js
:
- node index.js
ارجع إلى المتصفح وقم بزيارة http://localhost:3000/admin
. ستظهر لوحة التحكم.
في لوحة التحكم، يمكنك مراجعة نوع المهمة والبيانات التي تستهلكها، ومزيد من المعلومات حول المهمة. يمكنك أيضًا التبديل إلى علامات تبويب أخرى، مثل علامة التبويب تم الانتهاء منها للحصول على معلومات حول المهام المكتملة، وعلامة التبويب فشلت للحصول على معلومات أكثر حول المهام التي فشلت، وعلامة التبويب متوقفة مؤقتًا للحصول على معلومات أكثر حول المهام التي تم إيقافها مؤقتًا.
يمكنك الآن استخدام لوحة التحكم bull-board
لمراقبة الطوابير.
الختام
في هذه المقالة، قمت بتحويل مهمة تستغرق وقتًا طويلاً إلى طابور مهام باستخدام bullmq
. أولاً، بدون استخدام bullmq
، قمت بإنشاء تطبيق يحتوي على مهمة تستغرق وقتًا طويلاً ولديها دورة استجابة بطيئة. ثم استخدمت bullmq
لتحميل المهمة التي تستغرق وقتًا طويلاً وتنفيذها بشكل غير متزامن، مما يعزز دورة الطلب/الاستجابة. بعد ذلك، استخدمت bull-board
لإنشاء لوحة تحكم لمراقبة طوابير bullmq
في Redis.
يمكنك زيارة وثائق bullmq
لمعرفة المزيد حول ميزات bullmq
التي لم يتم تناولها في هذا البرنامج التعليمي، مثل جدولة المهام، وتحديد أولوية المهام، وإعادة محاولة المهام، وتكوين إعدادات القدرة التنافسية للعمال. يمكنك أيضًا زيارة وثائق bull-board
لمعرفة المزيد حول ميزات لوحة التحكم.