المحتوى عن 'webpack'.



مزيد من الخيارات

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المُحتوى


التصنيفات

  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • نصائح وإرشادات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • التجارة الإلكترونية
  • الإدارة والقيادة
  • السلوك التنظيمي
  • مقالات ريادة أعمال عامة

التصنيفات

  • PHP
    • Laravel
    • ووردبريس
  • جافاسكريبت
    • Node.js
    • jQuery
    • AngularJS
    • Cordova
    • React
  • HTML
    • HTML5
  • CSS
  • SQL
  • لغة C#‎
  • لغة C++‎
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • Sass
    • إطار عمل Bootstrap
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة Swift
  • لغة R
  • لغة TypeScript
  • ‎.NET
    • ASP.NET
  • سير العمل
    • Git
  • صناعة الألعاب
    • Unity3D
    • منصة Xamarin
  • سهولة الوصول
  • مقالات برمجة عامة

التصنيفات

  • تجربة المستخدم
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
    • كوريل درو
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • خواديم
    • الويب HTTP
    • قواعد البيانات
    • البريد الإلكتروني
    • DNS
    • Samba
  • الحوسبة السّحابية
    • Docker
  • إدارة الإعدادات والنّشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • مقالات DevOps عامة

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • التسويق بالرسائل النصية القصيرة
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • مقالات عمل حر عامة

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
  • أندرويد
  • iOS
  • macOS
  • ويندوز

التصنيفات

  • شهادات سيسكو
    • CCNA
  • شهادات مايكروسوفت
  • شهادات Amazon Web Services
  • شهادات ريدهات
    • RHCSA
  • شهادات CompTIA
  • مقالات عامة

أسئلة وأجوبة

  • الأقسام
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة البرمجة
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات
    • أسئلة الشهادات المتخصصة

التصنيفات

  • ريادة الأعمال
  • العمل الحر
  • التسويق والمبيعات
  • البرمجة
  • التصميم
  • DevOps

تمّ العثور على 2 نتائج

  1. شهد مجال تطوير الوِب في السنوات الأخيرة تطورات هائلة على جميع المستويات والذي أدى إلى تحسنيات كبيرة على مستوى السرعة والجودة وقابلية الصيانة. ليس ذلك فحسب بل تعدى الأمر إلى إمكانية أتمتة بعض المهام الروتينية والتركيز على جودة العمل المُسلم ورفع الإنتاجية وذلك باستخدام أدوات مساعدة مثل Webpack، والتي تعدّ من أشهر الأدوات المساعدة بل إنها أصبحت عنصرًا أساسيًا في أي مشروع يُبنى بلغة جافاسكربت. حدثت بعض هذه التطورات على صعيد تطوير الواجهات الخلفية (Back-end) مثل ظهور Node.js والّتي تستخدم استخدامًا كبيرًا في هذه الأيام، وبعضها الآخر كان في تطوير الواجهات الأمامية (front-end)، ومن أبرز هذه التطورات هي النقلة النوعية للغة جافاسكربت ودعمها للعديد من المميزات والخصائص، والتي أدت في نهاية المطاف لجعلها واحدةً من أبرز الخيارات القوية في برمجة الوِب. بل وحتى إنها احتلت المرتبة الأولى في ترتيب أشهر لغات البرمجة شعبية لعام 2019 وذلك بحسب إحصائية موقع Stackoverflow. وكما أعلنت شركة Web3Techs أن لغة جافاسكربت مُستخدمة من قِبل حوالي 95% من مواقع العالم قاطبةً، وهي أيضًا على رأس قائمة "أكثر لغة مشهورة في برمجة الواجهات الأمامية" بحسب نفس الشركة. ولكن كيف جرت هذه التحولات السريعة والقوية بنفس الوقت لهذه اللغة؟ وما هي المراحل الأساسية الّتي مرت بها؟ وما هي الأدوات المختلفة الّتي ظهرت بالتزامن مع تطور هذه اللغة؟ وما هي أدوات البناء (مثل Gulp)؟ وما هو مجمع الوحدات (مثل Webpack)؟ ولماذا ظهر؟ وما هي مميزاته؟ سنحاول في هذا الدليل مرافقتكم في رحلة استكشافية للإجابة على هذه الأسئلة، ولنستعرض فيها أيضًا التاريخ العريق لهذه اللغة، ومعرفة الاسباب الرئيسية لتطورها وكيف احتلت بهذه السرعة المكانة العالية الّتي هي عليها اليوم. فهل أنت مستعد لمشاركتنا في هذه الرحلة؟ لمحة تاريخية بالعودة إلى زمن ما قبل الأدوات الحديثة كيف كان الحال آنذاك؟ في الحقيقة كانت الأمور بسيطة وسهلةً جدًا. سنبدأ رحلتنا بالتعرف على طرق استخدام لغة جافاسكربت في ملفات HTML. طرق استخدام لغة جافاسكربت يوجد طرقٌ عديدة يمكننا لتضمين الشيفرات البرمجية للغة جافاسكربت في ملفات HTML، يعتمد اختيارنا للطريقة بحسب حجم المشروع الّذي سنعمل عليه وسنذكر أهم الطرق المستخدمة من البداية وحتى يومنا الحالي. الشيفرة السطرية بدأت لغة جافاسكربت في كتابة شيفرتها البرمجية بهذه الطريقة البسيطة إذ أن الشيفرة البرمجية مكتوبة مباشرة في ملف HTML داخل وسم <script>. لا بدّ بأن معظمنا كتبَ شيفرة برمجية بهذه الطريقة في بداية تعلمه لهذه اللغة وذلك من كونها أسهل الطرق لتطبيق شيفرة برمجية معينة. في المثال التالي يحتوي الملف index.html على شيفرة جافاسكربت سطرية: <html> <head> <meta charset="UTF-8"> <title>Inline Example</title> </head> <body> <h1> The Answer is <span id="answer"></span> </h1> <script type="text/javascript"> function add(a, b) { return a + b; } function reduce(arr, iteratee) { var index = 0, length = arr.length, memo = arr[index]; for(index += 1; index < length; index += 1){ memo = iteratee(memo, arr[index]) } return memo; } function sum(arr){ return reduce(arr, add); } /* Main Function */ var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ]; var answer = sum(values) document.getElementById("answer").innerHTML = answer; </script> </body> </html> هذه الطريقة جيدة جدًا للبدء إذ لا يوجد أي ملفات خارجية أو تبعية ناشئة بين الملفات، ولكن هذه هي الطريقة المثالية لبناء شيفرة برمجية غير قابلة للصيانة وذلك بسبب المشاكل التالية: عدم إمكانية إعادة استخدام الشيفرة البرمجية: فإذا احتجنا إلى إضافة صفحة أخرى وهذه الصفحة تحتاج إلى بعض الوظائف المحققة في الشيفرة البرمجية الّتي كتبناها سابقًا في الملف السابق فيجب علينا حينها نسخ الشيفرة البرمجية المطلوبة ولصقها في الصفحة المراد تحقيق الوظائف فيها. عدم وجود ثبات في التبعية بين الشيفرة البرمجية: أنت مسؤول عن تنسيق التبعية بين الدوال المستخدمة على سبيل المثال إن كان هنالك دالة معينة تعتمد في وظيفتها على نتيجة دالة أخرى محققة قبلها فيجب علينا حينها الانتباه لهذا الأمر وعدم تغيير ترتيب تواجد الدوال في الشيفرة البرمجية. التضارب في المتغيرات العامة (Global Variables): جميع الدوال والمتغيرات ستكون موجودة على نطاق عام (Global Scope) وهذا بدوره سيؤدي إلى تضارب في المتحولات، ويحدث هذا عند ازدياد ضخامة المشروع وكتابة شيفرة برمجية كبيرة مما يجعل إمكانية إعادة استخدام نفس أسماء المتحولات أمر وارد الحدوث بقوة، وخصيصًا إن كان هنالك أكثر من مبرمج يعمل على نفس المشروع. استخدام الملفات الخارجية الطريقة الأخرى المستخدمة في كتابة الشيفرة البرمجية للغة جافاسكربت هي استخدام ملفات منفصلة إذ سنجزّء الشيفرة البرمجية إلى أجزاء صغيرة ونحملها تباعًا في ملف index.html. وبهذه الطريقة يمكننا استدعاء مكتبات منفصلة والّتي تقدم لنا وظائف إضافية مثل مكتبة moment (والتي تتيح لنا التلاعب بالتواريخ وإمكانية إجراء تحويلات معينة من نمط تاريخٍ معين إلى نمطٍ آخر). وبالإضافة إلى ذلك أدى استخدام الملفات المنفصلة في الشيفرة البرمجية إلى إتاحة الفرصة لنا بإعادة استخدام الشيفرة البرمجية أكثر من مرة ومن أي مكان نريد، وبذلك نكون تخلصنا من فكرة نسخ الشيفرة البرمجية من مكان ما ولصقها في مكان آخر متى ما أردنا استخدام هذه الشيفرة. الملف index.html هو الملف الّذي سيستدعي جميع التبعيات (ملفات جافاسكربت) الّتي سيحتاجها ويكون محتواه على الشكل التالي: <html> <head> <meta charset="UTF-8"> <title>External File Example</title> <!-- الملف الرئيسي الّذي سيستدعي ملفات جافاسكربت --> </head> <body> <h1> The Answer is <span id="answer"></span> </h1> <script type="text/javascript" src="./add.js"></script> <script type="text/javascript" src="./reduce.js"></script> <script type="text/javascript" src="./sum.js"></script> <script type="text/javascript" src="./main.js"></script> </body> </html> إن هذا المثال يجمع عناصر المصفوفة الموجودة في الملف main.js ويظهرها في صفحة index.html ويكون محتوى الملفات على الشكل التالي: الملف add.js: function add(a, b) { return a + b; } الملف reduce.js function reduce(arr, iteratee) { var index = 0, length = arr.length, memo = arr[index]; index += 1; for(; index < length; index += 1){ memo = iteratee(memo, arr[index]) } return memo; } الملف sum.js function sum(arr){ return reduce(arr, add); } الملف main.js var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ]; var answer = sum(values) document.getElementById("answer").innerHTML = answer; ولكن هذا الحل لم يكُ مثاليًا بل واجه المشاكل التالية: عدم وجود ثبات في التبعية بين الشيفرة البرمجية. التضارب في المتغيرات العامة مازالت موجودة. تحديث المكتبات أو الملفات المستدعاة سيكون يدويًا في حال ظهور نسخة جديدة للمكتبة (أو الملف). زيادة عدد طلبات HTTP بين المخدم والعميل: يؤدي زيادة الملفات المتضمنة في إلى زيادة الحمل على المخدم. فعلى سبيل المثال في الشيفرة السابقة سيحتاج كلّ ملف من الملفات الأربع طلب HTTP منفصل (أي أربع رحلات ذهاب وعودة من المخدم وإلى جهاز العميل) وبناءً عليه سيكون من الأفضل لو استخدمنا ملفًا واحدًا بدلًا من أربع ملفات منفصلة. كما في المثال التالي: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>External File Example</title> <!-- الملف الرئيسي الّذي سيستدعي ملفات جافاسكربت --> </head> <body> <h1> The Answer is <span id="answer"></span> </h1> <script src="/dist/bundle.js"></script> </body> </html> استخدام كائن الوحدات (Module Object) ونمط الوحدات (Module Pattern) باستخدام نمط الوحدات (والذي تحدثنا عنه في مقال مفصّل) يمكننا أن نقلل التضارب في المتغيّرات العامة. إذ إننا سنكشف عن كائن واحد للمجال العام (Global Scope) وهذا الكائن سيحتوي على كافة الدوال والقيم الّتي سنحتاج إليها في تطبيق الوِب خاصتنا. في المثال التالي سنكشف عن كائن واحد وهو myApp للمجال العام (Global Scope)، وكلّ الدوال ستكون متاحة عن طريق هذا الكائن. والمثال التالي سيُوضح الفكرة: الملف my-app.js: var myApp = {}; الملف add.js: (function(){ myApp.add = function(a, b) { return a + b; } })(); الملف reduce.js: (function () { myApp.reduce = function (arr, iteratee) { var index = 0, length = arr.length, memo = arr[index]; index += 1; for (; index < length; index += 1) { memo = iteratee(memo, arr[index]) } return memo; } })(); الملف sum.js: (function () { myApp.sum = function (arr) { return myApp.reduce(arr, myUtil.add); } })(); الملف main.js: (function (app) { var values = [1, 2, 4, 5, 6, 7, 8, 9]; var answer = app.sum(values) document.getElementById("answer").innerHTML = answer; })(myApp); الملف index.html: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>JS Modules</title> </head> <body> <h1> The Answer is <span id="answer"></span> </h1> <script type="text/javascript" src="./my-app.js"></script> <script type="text/javascript" src="./add.js"></script> <script type="text/javascript" src="./reduce.js"></script> <script type="text/javascript" src="./sum.js"></script> <script type="text/javascript" src="./main.js"></script> </body> </html> لاحظ أن كلّ ملف من الملفات غُلّف بطريقة نمط الوحدات ما عدا my-app.js إذ أن طريقة التغليف العامة المتبعة في نمط الوحدات هي على الشكل التالي: (function(){ /*... your code goes here ...*/ })(); بهذه الطريقة نجد أن كلّ المتحولات المحلية ستبقى ضمن مجال الدالة المستخدمة فيه. وبذلك لن يكون هنالك أي احتمالية لحدوث تضارب في المتغيرات العامة. وهكذا نكون حللنا مشكلة التضارب في المتغيرات العامة. سيكون الوصول إلى الدوال المعرفة في الملفات مثل (add و reduce ..إلخ) عن طريق ربط اسم الدالة مع الكائن myApp كما في المثال التالي: myApp.add(1,2); myApp.sum([1,2,3,4]); myApp.reduce(add, value); كما يمكننا أيضًا تمرير الكائن myApp كوسيط مثل ما فعلنا في ملف main.js وذلك من أجل تصغير أسم الكائن وجعل الشيفرة البرمجية أصغر حجمًا. تعدّ هذه الطريقة تحسنًا ممتازًا بالموازنة مع المثال السابق وفي الحقيقة إن أشهر مكتبات جافاسكربت مثل jQuery تستخدم هذه الطريقة؛ إذ أنها فقط تكشف عن متغير عام وهو $ وكل التوابع تُستدعى عن طريق هذا الكائن. في الحقيقة إننا إلى هذه اللحظة لم نصل إلى الحل النهائي إذ أن الحل السابق لا يزال يعاني من بعض المشاكل مثل: عدم وجود ثبات في التبعية بين الشيفرة البرمجية: نلاحظ في ملف index.html الملفات لا تزال بحاجة إلى وضعها وفق ترتيب معين يحافظ على التبعية بين الملفات إذ أن الملف myApp.js يجب أن يأتي قبل أي ملف وملف main.js يجب أن يأتي بعد كلّ الملفات. التضارب في المتغيّرات العامة: صحيح أننا قللنا عدد المتغيّرات العامة إلى الواحد ولكنها لا تزال موجودة. تحديث المكتبات: إن تحديث المكتبات (أو التبعيات أو أي ملف عمومًا) المستدعاة سيكون يدويًا في حال ظهور نسخة جديدة للمكتبة أو للتبعية. زيادة عدد طلبات HTTP بين الخادم والعميل: وذلك بعد زيادة عدد الملفات المستدعاة في ملف index.html. ظهور CommonJS جرت نقاشات عديدة في عام 2009 حول إمكانية جلب لغة جافاسكربت إلى الواجهات الخلفية (استخدامها من جانب الخادم) وفعلًا كان ذلك على يد مهندسٍ شاب يعمل لدى شركة موزيلا ويُدعى كيفن دانجور (Kevin Dangoor). قدمت CommonJS طريقة لتعريف الوحدات لحل مشكلة المجال في جافاسكربت من خلال التأكد من تنفيذ كلّ وحدة في فضاء الأسماء (namespace) الخاص بها وذلك بإجبار الوحدات على تصدير تلك المتغيّرات صراحةً (الذين نريد عرضهم واستخدامهم في المجال العام) وأيضًا تعريف تلك الوحدات المطلوبة للعمل بالشكل الصحيح مع بعضها بعضًا. بالإضافة إلى ذلك قدمت CommonJS واجهة برمجية مخصصة للجافاسكربت هذه المميزات فتحت أبصار العالم لإمكانيات هذه اللغة العظيمة، وجذبت اهتمام كبير من المطورين والذي انعكس في سرعة تطورها وتأقلمها مع جميع التغيّرات والمشاكل الّتي واجهتها. إن CommonJS ليست مكتبة جافاسكربت وإنما هي معايير تنظيمية لكتابة الشيفرة البرمجية (على سبيل المثال الطريقة الّتي خصصتها لاستيراد الوحدات استيرادً منظمًا) وهذه المعايير شبيه بالّتي تطرحها منظمة ECMA ‏(European Computer Manufacturers Association). ولتحقيق التنظيم في طريقة استدعاء الوحدات تقدم لنا CommonJS الدوال والأغراض المساعدة لذلك وهي: دالة require()‎: والّتي تسمح لنا باستيراد وحدات معينة في المجال المحلي الّذي نكون فيه. كائن الوحدة module.exports: والذي يسمح لنا بتصدير دالة ما من المجال الحالي إلى الوحدة. ولنأخذ مثلًا بسيطًا عن كيفية استخدام CommonJS: الملف moduleA.js: module.exports = function( value ){ return value*2; } الملف moduleB.js: var multiplyBy2 = require('./moduleA'); var result = multiplyBy2( 4 ); لاحظ أننا لم نستخدم الاسم الكامل للملف moduleA.js وإنما حذفنا اللاحقة، كما أن /. تشير إلى أن الوحدة moduleA موجودة في نفس المجلد الموجود فيه moduleB. وبفضل مشروع CommonJS ظهر أكبر مشروع معتمدًا عليه وهو Node.js وهي بيئة تشغيل لغة جافاسكربت مفتوحة المصدر وتعمل على جميع أنظمة التشغيل، والّتي تستطيع تشغيل شيفرة جافاسكربت خارج المتصفحات. بالإضافة إلى ذلك تسمح Node.js للمطوري الواجهات الخلفية باستخدام جافاسكربت لكتابة برمجيات تعمل من جهة الخادم وذلك لتوليد صفحات الوِب توليدًا ديناميكيًا قبل إرسالها إلى المتصفح، وكما تستطيع أيضًا التعامل مع الملفات وقواعد البيانات، ومختلف أنظمة الشبكات وخدمات أنظمة التشغيل. الوحدة غير المتزامنة AMD علِمنا إن الهدف الرئيسي الّذي بنيت عليه CommonJS هو استخدامها في الواجهات الخلفية (من طرف المخدم) ولذلك مشكلة استدعاء الوحدات استدعاءً متزامنًا لم تكُ مشكلة في ذلك الحين، بل ولم تكُ لتظهر لولا إصرار مجتمع المطورين على جلب CommonJS إلى الواجهات الأمامية ولنُلخص مشكلة CommonJS هي عندما نستدعي الوحدة معينة في ملف ما ولتكن مثلًا add كما في السطر التالي: var add=require('add'); سيتوقف النظام حتى تصبح الوحدة add جاهزة؛ أي أن هذا السطر البرمجي سيجمد المتصفح أثناء تحميل هذا الملف. ماذا لو كان هنالك أكثر من وحدة مطلوبة كيف ستكون الأمور عندها؟ لذلك إنها ليست أفضل طريقة لتعريف الوحدات من جانب المتصفح. من أجل نقلِ صياغة تعريف وحدة ما من صياغة يفهمها الخادم إلى صياغة يفهمها المتصفح قدمت لنا CommonJS العديد من التنسيقات لتعريف الوحدات وكانت واحدة من أشهرها هي تعريف الوحدات بالطريقة غير المتزامنة (Asynchronous Module Definition) والّتي تُعرف إختصارًا AMD وتكون طريقة تعريفها كما في المثال التالي: define([‘add’, ‘reduce’], function(add, reduce){ return function(){...}; }); إن الدالة define تأخذ قائمة بالتبعيّات الموجودة والتي مرّرناها لها كقائمة وستُعيد النتيجة إلى الدالة المجهولة (Anonymous function) كوسطاء (الدالة المجهولة هي الدالة الّتي ليس لديها اسم ولقد فصلنا في مقالٍ سابق كيفية استخدمها وذلك في سلسلة تعلم جافاسكربت). وسيكون ترتيب التبعيات بنفس الترتيب الموجود في القائمة ومن ثم ستُعيد الدالة المجهولة النتيجة والتي سنُصدرها ونستخدمها في نهاية المطاف. هذه الطريقة تُكافئ الطريقة السابقة لاستدعاء الوحدات (التبعيات) لأنها تعطي نفس النتائج الوظيفية، إلا أن هذه الطريقة تستخدم التحميل غير المتزامن للوحدات. إن CommonJS و AMD حلّت آخر مشكلتين متبقيتين مع نمط الوحدات وهما على الشكل التالي: عدم وجود ثبات في التبعية بين الشيفرة البرمجية. التضارب في المتغيرات العامة. نحن فقط سنهتم بالتبعيات الموجودة من أجل كلّ وحدة (أو ملف) وبالتأكيد في هذه الحالة لا يوجد هناك أي تضارب في المتغيرات العامة. مُحمل الوحدات RequireJS في الحقيقة إن طريقة استخدام AMD مع CommonJS كانت مفيدة جدًا، ولكن كيف يمكننا استخدامها في المتصفح وهنا جاء محمل الوحدات RequireJS إلى الساحة والّذي سيساعدنا لتحميل الوحدات تحميلًا غير متزامن AMD. والذي يعتبر من أوائل التطبيقات الفعلية لمكتبة CommonJS مع AMD. على الرغم من إسمها، إلا أن هدفها ليس دعم بناء جملة 'require' في CommonJS. إن RequireJS تُتيح لنا كتابة وحدات من نمط AMD. ملاحظة: قبل أن تطبق المثال التالي، يجب عليك تنزيل ملف require.js من موقعه الرسمي. لاحظ أن الاستدعاء الوحيد الموجود في ملف index.html هو: <script data-main=”main” src=”require.js”> </script> هذا الوسم سيُحمِّلُ محليًا مكتبة require.js إلى الصفحة، ومن بعدها السمة data-main في الوسم <script> ستخبر المكتبة من أي ملف ستبدأ. بعد أن تستدعي require.js ملف main.js سيقودنا هذا الملف إلى التبعيات المتعلقة به، وهكذا إلى أن نحمل جميع التبعيات (الملفات) المطلوبة. ملاحظة: استخدمنا main فقط بدون الإشارة إلى اللاحقة والتي هي js وذلك لأن هذه المكتبة تفترض بأن كلّ القيم الّتي سنُسندها لهذه الخاصية ستكون ملفات جافاسكربت. وسيكون تحميل التبعيات للمثال السابق كما في الصورة التالية: نلاحظ أن المتصفح يحمّل الملف index.html والملف بدوره يحمّل المكتبة require.js والّذي سيتكفل ببقية الملفات الأخرى. وبهذا نكون حللنا جميع المشاكل الّتي واجهتنا حتى هذه اللحظة، ولكن وكما هي العادة في عالم البرمجة في كلّ محاولة لإصلاح مشكلةٍ ما تظهر مشكلةٌ أخرى، ولكن عادة ما تكون أخف من سابقتها في التأثير. ومن المشاكل الّتي ظهرت مع هذا الحل هي: صياغة AMD طويلة ومضجرة: إذ أن كلّ شيء يجب تغليفة بالدالة define ولذا يوجد مسافة بادئة (يُقصد تعريف الدالة define) للشيفرة البرمجية وهذه المشكلة يمكن ألا تظهر مع الملفات الصغيرة ولكن ما إن يكبر حجم الملف وتزداد تفاصيله ستصبح مشكلة كبيرة. قوائم الاعتماديات الموجودة في المصفوفة: إن قائمة الاعتماديات الموجودة في المصفوفة يجب أن تتشابه تمامًا مع القائمة المررة كوسيط للدالة وإذا كان لدينا العديد من التبعيات ستزداد الأمور تعقيدًا. في النسخة الحالية من HTTP للمتصفحات تحميل العديد من الملفات الصغيرة سيخفف من الأداء (وخصيصًا بدون استخدام أي تقنيات مساعدة مثل: ذاكرة التخزين المؤقت Cache). مجمع الوحدات أو مجمع الحزم (Module Bundler) انطلاقًا من الأسباب السابقة أراد بعض الأشخاص بناء التعليمات من خلال CommonJS لكن هذه الأخيرة مخصصة للعمل على الخوادم وليس المتصفح وهي تدعم التحميل المتزامن أيضًا. بقيت هذه الإشكالية عصية على الحل حتى جاء مجمع الوحدات Browserify لينقذ الموقف ويقدم لنا حلولً مناسبة. وهو يعدّ أول مجمع وحدات وجد آنذاك، وكان باستطاعته تحويل الوحدات المكتوبة بصياغة CommonJS والتي لا يستطيع المتصفح أن يفهمها بطبيعة الحال (وذلك لأنها بُنيت بالأساس للتعامل مع الخادم) إلى صياغة يفهمها المتصفح. يمكننا التعامل مع مجمع الحزم Browserify من خلال سطر الأوامر إذ يمكنك إعطاؤه أمرًا ما وتمرير بعض الخيارات الإضافية المتاحة لكل أمر من الأوامر الموجودة. وهو يعتمد في بنيته على مدير الحزم npm وبيئة Node.js. يمكن لمُجمع الوحدات Browserify تجميع شجرة التبعيات للشيفرة البرمجية الخاصة بك وتحزيمها في ملف واحد. وأيضًا يحل مشكلة عدم إمكانية الوصول إلى الملفات من قبل المتصفح إذ إنه يبحث عن جميع تعليمات require (والتي لا يمكن للمتصفح أن يستدعي الملف المطلوب لأنه لا يملك الصلاحيات المناسبة لذلك) فيستبدلها بمحتوى الملف المراد تضمينه (وذلك انطلاقًا من اعتماده على Node.js والتي تستطيع الوصول إلى الملفات) وهكذا يصبح الملف جاهزًا للتنفيذ في المتصفح ولا يواجه أي مشكلة. حلول كثيرة فأي منها سنستخدم؟ إذا لدينا الكثير من الخيارات المتاحة فأي واحدة منهم سنستخدم في مشروعنا القادم؟ إذ إننا حتى هذه اللحظة ناقشنا العديد من الخيارات المتاحة لكتابة الوحدات مثل: نمط الوحدات (Module Pattern) وطريقة CommonJS مع AMD وطريقة RequireJS فأي منهم سنعتمده لمشروعنا القادم؟ الجواب: ولا واحدة منها. كما نعلم بأن لغة جافاسكربت لم تكُ تدعم نظام الوحدات في أصل اللغة، ولهذا نلاحظ وجود العديد من الخيارات المتاحة لتعويض هذا النقص. ولكن هذا الأمر لم يستمر طويلًا إذ بعد تحديث المواصفات القياسية للغة جافاسكربت ES6 أصبح وأخيرًا نظام الوحدات جزءًا أساسيًا منها وهذا يكون جواب سؤالنا الرئيسي أي الخيارات سنستخدم؟ إنها طريقة ES6 (تحدثنا في سلسلة مقالاتٍ سابقة عن المميزات الجديدة لهذا الإصدار من لغة جافاسكربت ES6). تُستخدم التعليمة import و export لاستيراد وتصدير الوحدات في التحديث الجديد ES6. وإليك مثالًا عمليًا يوضح لك كيفية استخدامها: الملف main.js: import sum from "./sum"; var values = [ 1, 2, 4, 5, 6, 7, 8, 9 ]; var answer = sum(values); document.getElementById("answer").innerHTML = answer; الملف sum.js: import add from './add'; import reduce from './reduce'; export default function sum(arr){ return reduce(arr, add); } الملف add.js: export default function add(a,b){ return a + b; } الملف reduce.js: export default function reduce(arr, iteratee) { let index = 0, length = arr.length, memo = arr[index]; index += 1; for(; index < length; index += 1){ memo = iteratee(memo, arr[index]); } return memo; } في الحقيقة دعم جافاسكربت لنظام الوحدات بهذا الشكل المختصر قلبَ مجريات الُلعبة، بل إن نظام الوحدات هذا سيحكم عالم جافاسكربت في نهاية المطاف. إنها باختصار مستقبل لغة جافاسكربت. ولكن! (أعلم بأنك سئمت من هذه الكلمة ولكن يجب علينا إكمال بقية جوانب هذه الطريقة لتوضيح جميع تفاصيل المشهد لديك) هذا التحديث من اللغة لم تدعمه جميع المتصفحات المستخدمة في هذا اليوم، أو لنكن أكثر دقة ما تزال بعض المتصفحات لا تدعم هذه التحديث وخصوصًا المتصفحات القديمة وفي الحقيقة نسبة المستخدمين لهذه المتصفحات كبيرة وتؤخذ بعين الإعتبار. إذا ما الّذي يجب علينا فعله الآن؟ وكيف سنحوّل الشيفرة البرمجية من الإصدار ES6 إلى الإصدار ES5؟ هنا يأتي دور أهم أداة سنتعرف عليها اليوم ألا وهي مجمع الوحدات Webpack. ما هي Webpack؟ هي عبارة عن مجمع واحدات (Module Bundler) أو أداة بناء (Build Tool) مثل أداة Browserify وهي تحول شجرة تبعية الملفات وتُجمعها في ملف واحد. ليس ذلك فحسب وإنما تحوي على الكثير من المميزات الأخرى نذكر منها: تقسيم الشيفرة البرمجية: عندما يكون لدينا تطبيقات متعددة تتشارك نفس الوحدات يمكننا من خلال هذه الأداة تجميع الشيفرة البرمجية في ملفين أو أكثر. فمثلًا إذا كان لدينا التطبيقين التاليين app1 و app2 ويشترك كلاهما في العديد من الوحدات إذا استخدمنا Browserify سيكون لدينا app1.js و app2.js وكل ملف منهم مجمع فيه الشيفرات البرمجية الخاصة بكل تطبيق، بينما مع أداة Webpack يمكننا إنشاء ملف app1.js وملف app2.js وبالإضافة إلى الملف المشترك shared-lib.js. نعم ستضطر إلى تحميل ملفين في صفحة Html ولكن مع إمكانية استخدام تقنيات مساعدة مثل: التخزين في الذاكرة المؤقتة للمتصفح (Cache) واستخدام شبكة توصيل المحتوى الموزعة CDN ‏(Content Delivery Network) كلّ هذه التقنيات ستقلل من وقت التحميل الإبتدائي للصفحات. المُحملات (Loaders): مع استخدام المُحمل المخصص يمكنك تحميل أي ملف إلى التطبيق إذ يمكنك استخدام الدالة reuiqre ليس فقط لتحميل ملفات جافاسكربت فقط (مثلما كانت أداة Browserify) وإنما ملفات التنسيقات وملفات SaSS وملفات Less بالإضافة إلى إمكانية تحميل الصور والخطوط والملفات والكثير غيرها. الملحقات (Plugins): تقدم Webpack ميزة الملحقات مجموعة واسعة من المهام مثل تحسين الحزمة وإدارة الملحقات (مثل الصور) بالإضافة إلى ذلك يمكننا حتى التلاعب بالملفات قبل تحميلها وتحزيمها في الملف الهدف (على سبيل المثال إمكانية ضغط الصور قبل رفعها على المخدم سنتطرق لهذه الميزّة لاحقًا في هذا الدليل). نظام الأوضاع (Mode): توفر لك هذه الأداة ثلاث أنواع من الأوضاع وضع التطوير ووضع الإنتاج أو بدون وضع. باختصار يستخدم كلّ وضع من الأوضاع من أجل سيناريو معين: وضع التطوير: يستخدم عند العمل في مرحلة التطوير لمشروع الوِب وتكون الشيفرة البرمجية مقروءة ومفهومة. وضع الإنتاج: يستخدم عند العمل في مرحلة عرض مشروع الوِب على الإنترنت وتكون الشيفرة البرمجية مختصرة وغير مقروءة. بدون وضع: يكون الشيفرة الّتي استخدمت في مرحلة التطوير هي ذاتها الّتي ستعرض على الإنترنت بدون أي تعديل. تسهل عملية تحويل الشيفرات البرمجية إلى إصدار أقدم (Transpiling): إمكانية تحويل شيفرات جافاسكربت البرمجية الحديثة مثل ES6 والّتي لا تدعمها بعض المتصفحات إلى شيفرات جافاسكربت البرمجية القديمة مثل ES5 من خلال Babel على سبيل المثال، وبذلك نكون ضربنا عصفورين بحجر واحد أولهما أننا كتبنا التطبيق بالإصدار الحديث من اللغة، وثانيهما أننا استطعنا تشغيل التطبيق على كافة المتصفحات. أليس هذا رائعًا؟ لابد بأنك متحمسٌ جدًا للتعرف على هذه الأداة والبدء في استخدامها على الفور في مشاريعك القادمة إذًا هيا بنا نبدأ لنتعرف عليها أكثر ونتعلم كيفية استخدامها. مفاهيم أساسية قبل الشروع في العمل مع هذه الأداة لا بدّ لنا من التعرف على بعض المفاهيم الرئيسية والتي سنستخدمها بكثرة في الأمثلة ملف الإعداد webpack.config.js :هو عبارة عن ملف يحدد كيفية تعامل الأداة Webpack مع بعض الخصائص للمحملات أو الملحقات وبعض خصائص الأداة نفسها. وعند إنشائنا لمشروع جديد لن نجده في مجلد المشروع وذلك بسبب ميزة التعديلات الصفرية (zero configuration) الجديدة الّتي جاءت مع الإصدار الرابع أو الأحدث من هذه الأداة والتي تمكننا من استخدام الأداة بدون الحاجة لأي تعديل في ملف الإعداد. بل يمكننا عدم إنشاؤه من الأساس. يمكن تخصيص هذا الملف تخصيصًا بسيطًا أو متقدمًا وذلك بحسب المشروع الّذي تعكفُ على تطويره، وهنالك بعض الخصائص الأساسية المشتركة في كلّ المشاريع، والّتي يجب التنويه إليها: الخاصية Entry: تحدد هذه الخاصية ملف الأولي من الشروع الّذي سنبنيه (نجمعه) في أول التنفيذ. الخاصية Output: تحدد مجلد المشروع الّذي سنستخدمه في مرحلة الإنتاج (أو النشر) يكون فيه الشيفرة البرمجية الّتي نريد استخدامها لنشر المشروع على الإنترنت. تحتوي هذه الخاصية على بعض الخيارات نذكر منها: filename: نحدد فيها أسم الملف الّذي نريد تجميع الحزم فيه. path: نحدد فيها مسار الملف الّذي نريد تجميع الحزم فيه. الخاصية Loaders: تحدد المحملات الّتي نريد أن نستخدمها مع Webpack. كلّ مُحمل يحتوي على بعض الخيارات نذكر منها: test: تحدد نوع الملفات الّتي سيُحملها هذا المُحمل (يمكننا استخدام التعابير النمطية في تحديد الملفات). exclude: الملفات الّتي نريد استبعادها (والتي من الممكن أن تحقق الخاصية الأولى). use: ما هو المُحمل الّذي سنستخدمه لإجراء التغييرات على الملفات المقبولة في test ولم تُستبعد في الخاصية exclude. الخاصية Plugins: تحدد هذه الخاصية الملحقات الّتي نريد استخدامها وكما تحدد بعض الخيارات لهذه الملحقات. الخاصية Mode: تحدد هذه الخاصية الوضع الّذي نريد العمل عليه أي طريقة نشر المشروع إلى مجلد الخرج (output). كانت هذه أبرز المفاهيم الأساسية الّتي سنتعامل معها في الأمثلة القادمة. إعداد بيئة التطوير بما أن مجمعات الحزم (الوحدات) تعتمد على Node.js اعتمادً أساسيًا للوصول إلى نظام الملفات من أجل تجميع الاعتماديات بشكلها النهائي لذا لابد لنا من تثبيت Node.js في البداية وإليك الخطوات الكاملة لتثبيته على نظام التشغيل ويندوز (يمكنك الاطلاع على مقال سابق تحدثنا فيه عن كيفية تثبيت Node.js على نظام لينكس): ندخل إلى الموقع الرسمي الخاص ببيئة Node.js ونحمل النسخة الموافقة لنظام التشغيل الخاص بك (يفضل تحميل الإصدارات ذات الدعم الطويل والتي تُدعى LTS). بمجرد إنتهاء التنزيل نفتح الملف المُنزّل. سيسألك معالج التثبيت عن موقع التثبيت والمكونات الّتي تريد تضمينها في عملية التثبيت يمكنك ترك تلك الخيارات بوضعها الافتراضي. انقر فوق تثبيت وانتظر حتى ينتهي معالج التثبيت. تهانينا أصبحت بيئة Node.js جاهزة للعمل. ملاحظة: يمكنك التأكد من تثبيت البيئة تثبيتًا صحيحًا من خلال تنفيذ الأمر التالي على سطر الأوامر (Command line) node –v يجب أن يظهر لك نسخة Node.js المثبتة وفي حالتنا ستظهر 12.16.1. كما يمكنك التأكد من تثبيت مدير الحزم Node Package Manager والّذي يُعرف اختصارًا npm من خلال الأمر npm –v يجب أن تظهر لك نسخة npm المثبتة وفي حالتنا ستظهر 6.13.4. نحن جاهزون الآن لإنشاء مشروع جديد وتثبيت Webpack! إنشاء مشروع جديد وتثبيت Webpack في البداية ننشئ مجلدًا جديدًا للمشروع وندخل إليه عبر التعليمات التالية: $ mkdir project; cd project ثم ننشئ مشروعًا جديدًا عبر التعليمة التالية: $ npm init -y ثم نثبت أداة Webpack عبر التعليمة التالية: $ npm install -D webpack webpack-dev-server يجب أن يظهر لك نتيجة مماثلة للصورة التالية: الآن لننشئ بنية الملفات الهرمية التالية: webpack-demo |- package.json + |- index.html + |- /src + |- index.js ملاحظة: جرى عرض الملفات في هذه المقالة بأسلوب عرض التغييرات في git فالأسطر الّتي بجانبها إشارة موجبة + يجب عليك إضافتها والّتي بجانبها إشارة سالبة - يجب عليك حذفها. الآن نفتح ملف index.js الموجود في مجلد src ونجري عليه التعديلات التالية: unction component() { const element = document.createElement('div'); // Lodash, currently included via a script, is required for this line to work element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; } document.body.appendChild(component()); ونعدل الملف index.html الموجود في مجلد webpack-demo ونجري عليه التعديلات التالية: <!doctype html> <html> <head> <title>Getting Started</title> <script src="https://unpkg.com/lodash@4.16.6"></script> </head> <body> <script src="./src/index.js"></script> </body> </html> كما يجب علينا أن نعدل ملف package.json لتفعيل الوضع الخاص كي نمنع النشر العرضي للشيفرة البرمجية الخاصة بنا ليصبح على الشكل التالي: { "name": "webpack-demo", "version": "1.0.0", "description": "", + "private": true, - "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "webpack": "^4.20.2", "webpack-cli": "^3.1.2" }, "dependencies": {} } لمزيد من المعلومات حول التفاصيل الداخلية لملف package.json ننصحك بأخذ جولة في التوثيق الرسمي الخاص به. في المثال السابق هنالك تبعيات ضمنية إذ أن الملف index.js يعتمد على loadash (وهي مكتبة تساعد المبرمجين على كتابة شيفرة برمجية أكثر إيجازًا وتزيد من قابلية الصيانة) في عمله وبناءً عليه يجب أن يكون loadash مُضمن قبل أن تعمل الشيفرة البرمجية للملف index.js. ولكن إن الملف index.js لم يُصرح بوضوح عن حاجته لهذه المكتبة وإنما افترض أنها موجودة ضمن المتحولات العامة. في الحقيقة إن إدارة المشروع بهذه الطريقة تعدّ مشكلة وذلك للأسباب التالية: ليس من الواضح أن الشيفرة البرمجية للملف index.js تعتمد على مكتبة خارجية. إذا كانت التبعية مفقودة، أو ضُمنت في الترتيب الخطأ، فلن يعمل التطبيق بالطريقة الصحيحة. إذا ضُمنت تبعيةً ما ولكن لم تُستخدم، سيضطر المتصفح إلى تنزيلها مع أنها غير ضرورية. لنستخدم Webpack لإدارة هذه المشروع بدلًا من الطريقة الحالية. إنشاء حزمة باستخدام Webpack سنعدل بنية المشروع قليلًا لفصل الشيفرة البرمجية الّتي سنعمل عليها عن الشيفرة البرمجية الّتي سننشرها. وتجدر الإشارة إلى أن الشيفرة البرمجية الّتي سننشرها هي الناتج المصغّر والمحسّن لعملية البناء الناتجة عن الشيفرة الّتي كتبناها والتي ستُحمّل في نهاية المطاف في المتصفح. إذًا سنضع الشيفرة البرمجية الّتي سنعمل عليها في المجلد src أما الّتي سننشرها ستكون في مجلد dist أي ستكون بهذا الشكل: webpack-demo |- package.json + |- /dist + |- index.html - |- index.html |- /src |- index.js لتحزيم مكتبة loadash (والّتي هي تبعية في ملف index.html) باستخدام الملف index.js سنحتاج أولًا إلى تثبيت هذه المكتبة محليًا باستخدام الأمر التالي: npm install --save lodash عند تثبيت حزمة (مكتبة) معينة والّتي سنحتاجها في عملية التحزيم النهائية قبل النشر النهائي يجب علينا إضافة install --save أما إذا أردت تثبيت حزمة (مكتبة) معينة لمرحلة التطوير فقط يجب عليك إضافة install --save-dev. لمزيد من المعلومات ننصحك بالإطلاع على التوثيق الرسمي لمدير الحزم NPM. الآن لنضيف تعليمة استيراد الحزمة في الشيفرة البرمجية للملف index.js الموجود في المجلد src كما في الشكل التالي: + import _ from 'lodash'; + function component() { const element = document.createElement('div'); - // Lodash, currently included via a script, is required for this line to work element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; } document.body.appendChild(component()); بما أننا قمنا باستيراد مكتبة loadash في الملف السابق لنعدل الملف index.html بما يتوافق مع ذلك ليكون كما في الشكل التالي: <!doctype html> <html> <head> <title>Getting Started</title> - <script src="https://unpkg.com/lodash@4.16.6"></script> </head> <body> - <script src="./src/index.js"></script> + <script src="main.js"></script> </body> </html> ملاحظة: يتطلب ملف index.js صراحة وجود المكتبة (أو أي تبعية إذا أردنا تعميم الفكرة) loadash ويربطه كمتحول _ (أي لا يوجد تضارب في المجال). من خلال تحديد التبعيات الّتي تحتاجها الوحدة، يمكن لمُجمع الوحدات (المقصود Webpack) استخدام هذه المعلومات لإنشاء مخطط بياني للاعتماديات (dependency graph). ثم يستخدم الرسم البياني لإنشاء حزمة محسنة والّتي ستُنفذ الاستدعاءات بالترتيب الصحيح. لننفذ التعليمة التالية npx webpack والتي ستأخذ الشيفرة البرمجية في الملف index.js وهو القيمة المسندة للخاصية entry وتنشر الخرج في ملف main.js والذي سيكون القيمة في المسندة للخاصية output. إن تعليمة npx والتي تأتي مع نسخة 8.2 من Node.js أو أحدث، و 5.2.0 من مدير الحزم NPM أو أحدث. إذ تعمل على أداة Webpack الثنائية الموجودة في (‎./node_modules/.bin/webpack) الّتي ثبتناها في البداية. ويكون ناتج التنفيذ كما يلي: npx webpack ... Built at: 13/06/2018 11:52:07 Asset Size Chunks Chunk Names main.js 70.4 KiB 0 [emitted] main ... WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/ ملاحظة: من الممكن أن يختلف الناتج الّذي سيظهر لك بعض الشيء ولكن إذا نجحت عملية البناء فلا تقلق من رسالة التحذير فإن الأمور على ما يرام. افتح الملف index.html يجب أن يظهر لك Hello webpack فإذا ظهرت لديك تهانينا هذا يدلّ على أنك أنجزت جميع الخطوات بنجاح. إذا ظهرت لديك رسالة خطأ في صياغة ملف جافاسكربت المصغّرة (minified) وذلك عند فتحك لملف index.html فعيّن وضع التطوير ومن ثم شغل الأمر npx webpack من جديد. هذا الخطأ يتعلق بتشغيل حزمة الوٍب npx على أحدث نسخة من Node.js (الإصدار 12.5 أو الأحدث) بدلًا من الإصدار ذو الدعم الطويل LTS. الوحدات في المواصفات القياسية لنسخة جافاسكربت ES6 دُعمت تعليمتي import وexport إذ أن معظم المتصفحات تدعمها في الوقت الحالي (ولكن يوجد بعض المتصفحات لا تدعمها) وWebpack ليست استثناءً بل إنها تدعمها دعمًا متميزًا. تدعم Webpack هذه الخاصية من خلال تحويل (transpiles) الشيفرة البرمجية المقابلة للتعليمتين import وexport إلى نسخة أقدم من ES6 مثل ES5 وبذلك تؤمن فهم المتصفح ما تعنيه هذه التعليمتين باللغة الّتي يفهمها. إضافةً إلى ذلك يدعم Webpack العديد من صيغ الوحدات الأخرى لمزيد من المعلومات يمكنك زيارة التوثيق الرسمي. ملاحظة: إن Webpack لن يحوّل أي تعليمة برمجية عدا تعليمتي import وexport ولذلك في حال كنت تستخدم مميزات أخرى من المواصفات القياسية ES6 فعندها يجب عليك استخدام محولات المخصصة لذلك مثل Babel أو Bublé. استخدام ملف الإعداد إن جميع الإصدارات الّتي جاءت بعد الإصدار الرابع من Webpack تدعم ميزة التعديلات الصفرية الّتي تحدثنا عنها سابقًا في هذا الدليل، ولذا إذا أردنا أن نخصص بعض الإعدادات في المشروع لا بد لنا من إنشاء ملف الإعداد إنشاءً يدويًا. إذًا سنضيف ملف الإعداد على بنية الملفات وستصبح البنية الهرمية للمشروع على الشكل التالي: webpack-demo |- package.json + |- webpack.config.js |- /dist |- index.html |- /src |- index.js وسنُضيف إليه بعض التعليمات المهمة والتي سنستخدمها كثير في المشروع وبذلك ستتحسن إنتاجيتنا أكثر من ذي قبل. وتكون الإضافة كما يلي: const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'main.js', path: path.resolve(__dirname, 'dist'), }, }; أما الآن لنجرب تنفيذ التعليمة التالية: npx webpack --config webpack.config.js ... Asset Size Chunks Chunk Names main.js 70.4 KiB 0 [emitted] main ... WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/ ملاحظة: في حالة وجود أكثر من ملف للإعداد تلتقط التعليمة Webpack ذلك إلتقاطًا إفتراضيًا. ونستخدم خيار ‎--config وبعده اسم الملف لتوضيح فكرة أنه بإمكانك تمرير أسم أي ملف تريده تمريرًا يدويًا، وهذا سيكون مفيدًا جدًا لملفات الإعداد الأكثر تعقيدًا والتي سنحتاج لتقسيمها إلى ملفات متعددة. يتيح ملف التكوين مرونة أكبر بكثير من استخدام سطر الأوامر CLI ‏(Command Line Interface) البسيط. إذ يمكننا تحديد وتثبيت قواعد معينة للمُحمل (Loader) والملحقات (Plugin) وتخصيص الخيارات بالإضافة للعديد من التحسينات الأخرى. لمزيد من المعلومات يمكنك الإطلاع على التوثيق الرسمي لملف الإعداد. إنشاء إختصار لتعليمة البناء يمكننا استخدام مميزات Webpack لجعل الأمور أسهل من خلال وضع بعض اللمسات الجمالية مثل اختصار بعض الخطوات، وذلك من خلال كتابة الأوامر في ملف package.json وتحديدًا في scripts. وبذلك نختصر على أنفسنا عناء كتابة بعض التعليمات الطويلة والمُملة. إذًا لنعدل ملف package.json ليصبح على الشكل التالي: { "name": "webpack-demo", "version": "1.0.0", "description": "", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "build": "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "webpack": "^4.20.2", "webpack-cli": "^3.1.2" }, "dependencies": { "lodash": "^4.17.5" } } يمكننا الآن استخدام الأمر npm run build بدلًا من الأمر npx الّذي استخدمنا سابقًا. لاحظ بأنه يمكننا دائمًا الإشارة إلى الحزم المثبتة محليًا في مدير الحزم npm في السمة scripts في الملف package.json بنفس الطريقة الّتي استخدمناها مع التعليمة npx. هذا الطريقة هي متعارف عليها في معظم المشاريع القائمة على مدير الحزم npm لأنه يسمح لجميع المساهمين لاستخدام نفس التعليمات المشتركة (وحتى مع بعض الخيارات مثل ‎--config). ملاحظة: يمكنك تمرير الخيارات الخاصة لتعليمة ما من خلال إضافة شرطتين -- بين الأمر npm run build والخيارات الّتي نريد تخصيصها مثل npm run build -- --colors. npm run build ... Asset Size Chunks Chunk Names main.js 70.4 KiB 0 [emitted] main ... WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/. أما الآن وبعد أن تعلمنا الأساسيات عن كيفية استخدام Webpack لننتقل إلى الاستخدامات الأهم لهذه الأداة مثل إدارة الصور والخطوط ..إلخ. إذا نفذت جميع التعليمات السابقة تنفيذًا صحيحًا يجب أن يكون البنية الهرمية للمشروع على الشكل التالي: webpack-demo |- package.json |- webpack.config.js |- /dist |- main.js |- index.html |- /src |- index.js |- /node_modules ملاحظة: إذا كنت تستخدم الإصدار الخامس من مدير الحزم npm من الممكن أن ترى ملفًا إضافيًا اسمه package-lock.json. استخدام Webpack لإدارة الملحقات بعد أن تعلمنا أساسيات Webpack وإنشأنا مشروعًا صغيرًا يعرض "Hello Webpack"، سنحاول الآن أن نركز أكثر على الأمور المهمة والتي تخدمنا بها هذه الأداة مثل إدارة ملفات التنسيق والصور وسنرى بالضبط كيف تتعامل هذه الأداة مع كلٍّ منهم. يستخدم مطورو الواجهات الأمامية أدوات بناء مثل: Grunt و Glup لمعالجة هذه الملحقات ونقلها من مجلد src إلى مجلد dist أو حتى لإنشاء مجلد جديد لهذه الملحقات، تستخدم Webpack نفس الفكرة للتعامل مع الوحدات، بالإضافة إلى أن Webpack تجمع كلّ التبعيات ديناميكيًا وتنشئ ما يعرف بمخطط التبعيات وهذا أمر رائع لأن كلّ وحدة (Module) توضح صراحة ما هي تبعياتها وبذلك نتجنب تجميع الوحدات غير المستخدمة في المشروع. واحدة من أروع مميزات Webpack هي إعطاؤك إمكانية تضمين أي نوع آخر من الملفات، إلى جانب جافاسكربت، ولهذه الفكرة تحديدًا أنشئ المحمل (Loader). أي يمكن تطبيق نفس المزايا المذكورة أعلاه (التبعيات المستخدمة من قبل الوحدات) على كلّ شيء مستخدم في إنشاء المواقع أو تطبيقات الوِب أيضًا. إدارة ملفات التنسيق CSS في البداية سنعدل قليلًا في بنية الملفات وبعض الملفات قبل أن ندير ملفات التنسيق. سنعدل الملف dist/index.html ليصبح كما يلي: <!doctype html> <html> <head> - <title>Getting Started</title> + <title>Asset Management</title> </head> <body> - <script src="main.js"></script> + <script src="bundle.js"></script> </body> </html> وكما سنعدل أيضًا ملف الإعداد webpack.config.js ليصبح: const path = require('path'); module.exports = { entry: './src/index.js', output: { - filename: 'main.js', + filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, }; وبذلك يُصبح كلّ شيء جاهز لنبدأ. أولًا لنثبت محمل ملفات التنسيق css-loader الّذي سيساعدنا في معالجة تعليمة الاستيراد import الّتي سنستخدمها أيضًا من أجل استيراد ملفات التنسيق ومحمل التنسيق style-loader الّذي سيساعدنا في أخذ ملف التنسيق ووضعه في ملف (أو ملفات) تنسيق منفصلة. إذًا لنُنفذ الأمر التالي: npm install --save-dev style-loader css-loader ولنُعدل ملف الإعداد webpack.config.js ليحوي المعلومات اللازمة لحزم المُحمل (Loader) الّتي ثبتناها للتو وسيصبح الملف على الشكل التالي: const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, + module: { + rules: [ + { + test: /\.css$/, + use: [ + 'style-loader', + 'css-loader', + ], + }, + ], + }, }; ملاحظة: تستخدم Webpack التعابير النمطية (Regular Expression) لتحديد الملفات الّتي يجب البحث عنها وتقديمها إلى مُحمل (Loader) محدد. في الحالة السابقة نلاحظ أنه ستُقدم جميع ملفات التنسيق (ذات اللاحقة css) إلى المُحمل style-loader والمُحمل css-loader. وهذا بدوره سيمكنك من استيراد ملف التنسيق الّذي سيكون مثل هذا import './style.css' إلى الملف الّذي يعتمد على ملف التنسيق هذا. عند تشغيل مجمع الحزم Webpack سيُضاف ملف تنسيق مخصص في وسم <style> وذلك في داخل الوسم <head>. لنجرب ذلك من خلال إضافة ملف تنسيق جديد style.css إلى مشروعنا واستيراده في الملف index.js: لننشئ أولًا ملف التنسيق style.css في مشروعنا لتصبح البنية الهرمية للمشروع على الشكل التالي: webpack-demo |- package.json |- webpack.config.js |- /dist |- bundle.js |- index.html |- /src + |- style.css |- index.js |- /node_modules ومن ثم لنُضف بعض الخصائص إلى ملف style.css ليصبح على الشكل التالي: .hello { color: red; } ولنستدعي ملف التنسيق style.css في ملف index.js ليصبح على الشكل التالي: import _ from 'lodash'; + import './style.css'; function component() { const element = document.createElement('div'); // Lodash, now imported by this script element.innerHTML = _.join(['Hello', 'webpack'], ' '); + element.classList.add('hello'); return element; } document.body.appendChild(component()); ومن ثم نُنفذ أمر البناء التالي: npm run build ... Asset Size Chunks Chunk Names bundle.js 76.4 KiB 0 [emitted] main Entrypoint main = bundle.js لنفتح الآن ملف index.html ولنرى ما هي التغييرات الّتي جرت عليه. لاحظ أن كلمة "Hello Webpack" أصبحت الآن باللون الأحمر. لمعرفة ما الّذي فعلته Webpack افحص الصفحة من خلال أدوات المطوّر (المُقدمة من متصفح جوجل كروم على سبيل المثال)، ولكن لا تعرض مصدر الصفحة لأنها لن تقدم لك النتيجة الّتي نود الإشارة إليها. إذ أن الوسم <style> سيُنشئ ديناميكيًا من قِبل جافاسكربت. لذلك افحصها حصرًا من خلال أدوات المطوّر وافحص تحديدًا الوسم <head> يجب أن يحتوي على الوسم <style> والذي استوردناه في ملف index.js. ملاحظة: سابقًا كان علينا تصغير (Minimize) ملفات التنسيق تصغيرًا يدويًا قدر الإمكان وذلك بحذف المساحات الفارغة بين الخصائص الموجودة في الملف من أجل زيادة سرعة تحميل الصفحة، ولكن مع الإصدار الرابع من مجمع الحزم 4 Webpack أو الأحدث منه أصبح تصغير ملفات التنسيق خطوة افتراضية في الأداة، وبذلك أضافت لنا هذه الأداة بُعدًا آخر لتحسين الشيفرة البرمجية وسبب وجيّه لزيادة محبتنا لها. إدارة ملفات SASS تتيح لنا Webpack إمكانية التعامل مع ملفات SASS وتحويلها إلى ملفات تنسيق عادية. ولنأخذ مثالًا عمليًا نطبق فيه كيفية تحويل ملفات SASS إلى ملفات تنسيق عادية. سنركز في هذا المثال على دعم الملفات SCSS مثل: (your-styles.scss) و الملفات SCSS modules مثل: (your-component.module.scss). تجنبًا لتعقيد الأمور أكثر من اللازم سنبدأ بمشروع جديد ننفذ فيه هذه الفكرة. سنفترض أنك أنشأت مشروعًا جديدًا وثبتّ أداة Webpack، لننتقل الآن لتثبيت الإضافات والمُحملات اللازمة لهذا المشروع. سنبدأ أولًا بتثبيت الحزم التالية: npm install --save-dev node-sass sass-loader style-loader css-loader mini-css-extract-plugin ستكون مهمة كلّ حزمة من الحزم على الشكل التالي: node-sass: ستوفر هذه الحزمة ربط Node.js مع LibSass وهذا الأخير هو مترجم SASS. sass-loader: هو مُحمّل ملفات SASS إلى مشروعنا. css-loader: تستخدم هذه الحزمة لتفسير التعليمة @import و @url() بما تشير إليه في ملفات التنسيق. style-loader: تستخدم هذه الحزمة لإسناد الخصائص الموجودة في ملفات التنسيق إلى الوسوم الفعلية في شجرة DOM الافتراضية. mini-css-extract-plugin: هذه الحزمة تستخرج الخصائص الموجودة في ملفات التنسيق إلى ملفات منفصلة. إذ أنها تنشئ لكل ملف جافاسكربت ملف تنسيق الخاص به وهو لدعم ميّزة التحميل عند الطلب (On-Demand-Loading) لملفات التنسيق و خرائط الشيفرة البرمجية المحوّلة Source Maps (لمزيد من المعلومات عنها يمكنك الإطلاع على المقال التالي). سنحتاج إلى إضافة مُحملين، أحدهما للتنسيقات العامة والأخرى للتنسيقات المركبة. واللّذان يشار إليهما بملفات (SCSS modules). إذ أن هذه الأخيرة تعمل جيدًا مع المكتبات أو أطر العمل المُعتمدة على المكونات (component-based) مثل: React. سنضيف هذه الحزمة الملحقة إلى ملف الإعداد webpack.config.js على الشكل التالي: + const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { plugins: [ + new MiniCssExtractPlugin({ + filename: isDevelopment ? '[name].css' : '[name].[hash].css', + chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css' + }) ] } نلاحظ أننا أضفنا خاصية أسماء الملفات المجزأة من أجل زيادة فعالية وسهولة خرق ذاكرة التخزين المؤقّت (Cache Bustring وهي تحِلُّ مشكلة التخزين المؤقت في المتصفح باستخدام إصدار معرف فريد للملف يميزه وذلك من أجل أخباره في حال وجود نسخة جديدة من الملف الذي حفظه فعلًا في هذه الذاكرة، وذلك من أجل أن يحملها تلقائيًا إلى ذاكرة التخزين المؤقت بدلًا من نسخة الملف القديمة الموجودة فيه)، والآن سنضيف الخواص المناسبة لملف الإعداد webpack.config.js من أجل عملية التحويل المناسبة للملفات: module.exports = { module: { rules: [ + { + test: /\.module\.s(a|c)ss$/, + loader: [ + isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader, + { + loader: 'css-loader', + options: { + modules: true, + sourceMap: isDevelopment + } + }, + { + loader: 'sass-loader', + options: { + sourceMap: isDevelopment + } + } + ] + }, + { + test: /\.s(a|c)ss$/, + exclude: /\.module.(s(a|c)ss)$/, + loader: [ + isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader, + 'css-loader', + { + loader: 'sass-loader', + options: { + sourceMap: isDevelopment + } + } + ] + } ] }, resolve: { - extensions: ['.js', '.jsx'] + extensions: ['.js', '.jsx', '.scss'] } } نلاحظ أن القاعدة الأولى المطبقة في الشيفرة السابقة ستُطبق على الملفات ذات الامتدادات .module.scss أو .module.sass إذ في البداية ستُحوّلُ ملفات SASS إلى CSS من خلال sass-loader ومن ثم ستُمرّر إلى الحزمة css-loader لمعالجة التعليمات ‎@import()‎ و url()‎ ..إلخ. ومن ثمّ ستُسندُ الحزمة style-loader الخصائص المناسبة في DOM أو ستسند من خلال الحزمة Mini CSS Extract Plugin وذلك لإخراج ملفات التنسيق أثناء وضع النشر. القاعدة الثانية مشابهة جدًا للقاعدة الأولى باستثناء أننا لا نحوّل أسماء الأصناف. الآن أصبح ملف الإعداد جاهز نحتاج الآن لإنشاء ملف تنسيقات SASS للتأكد من أن كل شيء يعمل مثلما خطط له. أنشئ مجلد src ثم أنشئ بداخله ملفًا جديدًا باسم app.module.scss وأضف الشيفرة البرمجية التالية: .red { color: red; } أي عنصر سيأخذ الصنف red سيكون لونه أحمر. افتح الملف app.js واستدعي الملف السابق على الشكل التالي: import styles from './app.module' نلاحظ أن اسم الملف لا يحتوي على اللاحقة .scss وذلك لأننا سبق وأخبرنا Webpack بأن يأخذ بعين الاعتبار هذه اللاحقة وذلك في ملف الإعداد. ثم لنُضيف الآن الدالة التالية في نفس الملف app.js ليصبح على الشكل التالي: function App() { return <h2 className={styles.red}>This is our React application!</h2> } أليست سهلة جدًا؟ بغض النظر عن كيفية تحويل الحزمة css-loader اسم الصنف الّذي بنيناه (red) والّذي سيصبح مثل: ‎_1S0lDPmyPNEJpMz0dtrm3F أو شيء من هذا القبيل، ولكنها ساعدتنا في مهمتنا مساعدةً كبيرة. لنُضيف الآن بعض التنسيقات العمومية. لننشئ ملف جديد وليكن global.scss في المجلد src ولِنفتحه ونُضيف بداخله الشيفرة البرمجية التالية: body { background-color: yellow; } هذا الملف سيجعل لون خلفية الصفحة الرئيسية لتطبيق الوِب خاصتنا أصفر. ولكن يجب أن نفتح الملف index.js ونستدعي ملف التنسيق السابق في بداية الملف. مثلما هو موضح في الشيفرة التالية: import './global' واخيرًا سنحصل على الخرج التالي: نلاحظ أن استخدام وحدات SCSS يتطلب منا جهدًا لا بأس به، ولكن بالموازنة مع كمية الفائدة الّتي تقدمها وحدات SCSS من سهولة في الصيانة لتطبيق الوِب في المستقبل فأعتقد أن الأمر يستحق هذا الجهد. بالإضافة إلى ذلك توفر أداة Webpack حزم أُخرى مخصصة لأي نكهة من نكهات ملفات التنسيق مثل الحزمة: postcss لتوفير دعم Postcss أو الحزمة Less لدعم ملفات التنسيق من نوع Less أيضًا. إدارة الصور تتيح Webpack لنا إمكانية إدارة الصور مثل: الخلفيات والأيقونات وذلك من خلال مُحمل الملفات. في البداية لنثبت أولًا مُحمل الملفات من خلال الأمر التالي: npm install --save-dev file-loader ومن ثم سنضيف إلى ملف الإعداد webpack.config.js المعلومات اللازمة لعمل هذا المُحمل كما في الشكل التالي: const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ], }, + { + test: /\.(png|svg|jpg|gif)$/, + use: [ + 'file-loader', + ], + }, ], }, }; عند استيراد لصورة معينة ولتكن import MyImage from './my-image.png' ستضاف هذه الصورة إلى مجلد الخرج (output) الّذي حددناه في ملف الإعداد وسيحمل المتغير MyImage عنوان الرابط التشعبي الخاص بالصورة (URL) بعد معالجتها. عندما استخدمنا محمل ملفات التنسيق css-loader جرى نفس السيناريو السابق. أي أنه سيكون الخاصية التالية في ملفات التنسيق url('./my-image.png') والمُحمل سيلاحظ أن الملف هذا موجود محليًا وبذلك سيُحول '‎./my-image.png' إلى المسار النهائي المُسند للخاصية (output) في ملف الإعداد webpack.config.js ومُحمل ملفات html‏ (html-loader) سيتعامل مع <img src="./my-image.png" /‎> بنفس الطريقة. لنُضف الآن الصور اللازمة في مشروعنا. وهنا يمكنك استخدام أي صورة تريدها. لننسخ الصورة الّتي سنعمل عليها ولتكن icon.png إلى مشروعنا. لتصبح البنية الهرمية للمشروع على الشكل التالي: webpack-demo |- package.json |- webpack.config.js |- /dist |- bundle.js |- index.html |- /src + |- icon.png |- style.css |- index.js |- /node_modules ومن ثم لنستورد هذه الصورة في ملف src/index.js، ولنُضيفها إلى وسم <div> المتواجدة فيه، ولتصبح الشيفرة البرمجية للملف src/index.js على الشكل التالي: import _ from 'lodash'; import './style.css'; + import Icon from './icon.png'; function component() { const element = document.createElement('div'); // Lodash, now imported by this script element.innerHTML = _.join(['Hello', 'webpack'], ' '); element.classList.add('hello'); + // Add the image to our existing div. + const myIcon = new Image(); + myIcon.src = Icon; + + element.appendChild(myIcon); return element; } document.body.appendChild(component()); ومن ثم سنعدل ملف التنسيق من أجل تضمين الصور الّتي نعمل عليها ليصبح بذلك ملف التنسيق src/style.css على الشكل التالي: .hello { color: red; + background: url('./icon.png'); } ولننفذ الآن أمر البناء التالي: npm run build ... Asset Size Chunks Chunk Names da4574bb234ddc4bb47cbe1ca4b20303.png 3.01 MiB [emitted] [big] bundle.js 76.7 KiB 0 [emitted] main Entrypoint main = bundle.js ... إذا نفذت جميع الخطوات السابقة تنفيذًا صحيحًا يجب أن ترى الأيقونة (الصورة) مكررة في الخلفية، بالإضافة إلى وسم <img> بجوار النص "Hello webpack". وإذا فحصت الصورة ستجد أن اسمها الفعلي تغيّر إلى شيء شبيه بهذا الاسم 5c999da72346a995e7e2718865d019c8.png هذا يعني أن Webpack عثرت على الملف في مجلد src وعالجته. الخطوة المنطقية التالية الّتي سننفذها هي تصغير حجم الصور الّتي سنستخدمها في المشروع وتحسينها وذلك من خلال الحزم المساعدة المتوفرة مع الأداة Webpack مثل الحزمة Imagemin. ضغط الصور باستخدام Imagemin إن طريقة ضغط الصور الّتي سنستخدمها في المشروع هي باستخدام حزمة Imagemin إذ تعدّ هذه الحزمة خيارًا ممتازًا وذلك لأنها تدعم مجموعة متنوعة من أنواع الصور وسهلة التكامل مع الشيفرة البرمجية لأدوات البناء أو مجمع الحزم (الوحدات). سنعمل على مشروع جديد تجنبًا للمشاكل التي من الممكن أن تظهر لنا في حال أكملنا العمل على المشروع السابق. سنفرض أنك أنشأت مشروعًا جديدًا وثبّت الأداة Webpack تثبيتًا صحيحًا. في البداية لنثبت هذه الحزم عبر التعليمة التالية: npm install imagemin-webpack-plugin copy-webpack-plugin --save-dev ملاحظة: أن الحزمة copy-webpack-plugin ستساعدنا على نسخ الصور من مجلد images/ إلى مجلد dist/ وهو مجلد النشر وهو اختصار لكلمة distribution. لننشئ ملف الإعداد webpack.config.js ولنعدله ليتناسب مع الحزم الجديدة ليصبح على الشكل التالي: const ImageminPlugin = require('imagemin-webpack-plugin').default; const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require('path'); module.exports = { entry: './index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new CopyWebpackPlugin([{ from: 'img/**/**', to: path.resolve(__dirname, 'dist') }]), new ImageminPlugin() ] } تعدّ هذه الطريقة من أسهل الطرق لإعداد لهذه الحزمة. ولنضغط الصور الّتي في المجلد images/ وننسخها إلى المجلد dist/ من خلال تنفيذ الأمر التالي: webpack --config webpack.config.js --mode development نلاحظ من التعليمة السابقة أن وضع التنفيذ الحالي هو وضع التطوير، ولكن ما الّذي سيحدث إذا نفذنا التعليمة في وضع النشر؟ لنرى ما الّذي سيحدث. webpack --config webpack.config.js --mode production ستُظهر لنا في هذه المرة أداة Webpack تحذيرًا يخبرك بأن الصور ذات النوعية PNG الخاصة بك لا تزال تتجاوز الحد الأقصى للحجم الموصى به، على الرغم من بعض الضغط. ولذلك سنحتاج إلى ضبط اليدوي لعملية الضغط لهذا النوع من الصور لتصبح أفضل مما قبل. لنعدل قيمة الضغط للصور ذات النوعية PNG لتصبح 50% وسيكون ملف الإعداد على الشكل التالي: const ImageminPlugin = require('imagemin-webpack-plugin').default; const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require('path'); module.exports = { entry: './index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new CopyWebpackPlugin([{ from: 'img/**/**', to: path.resolve(__dirname, 'dist') }]), new ImageminPlugin({ pngquant: ({quality: [0.5, 0.5]}), }) ] } نلاحظ أننا مررنا للغرض Pngquant مجالًا لقيمة جودة الصور من 0.5 إلى 0.5 أي من الحدّ الأدنى إلى الحدّ الأعلى لجودة الصورة. إذ أن الحدّ الأعلى الأفتراضي 1 والحدّ الأدنى الافتراضي 0. ولكن ماذا لو أردنا أن نضبط الجودة (نسبة الضغط) المستخدمة لبقية الأنواع من الصور بنفس الطريقة السابقة؟ في الحقيقة يوجد بعض الملحقات الإضافية المساعدة لهذا الغرض تحديدًا والّتي سنستعرضها في هذا الجدول: نوع الصور ضغط مع خسارة بعض معلومات الصورة ضغط بدون خسارة بعض معلومات الصورة JPEG imagemin-mozjpeg imagemin-jpegtran PNG imagemin-pngquant imagemin-optipng GIF imagemin-giflossy imagemin-gifsicle SVG Imagemin-svgo WebP imagemin-webp 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; } نلاحظ أن لدينا نوعين من طرق ضغط الصور وهما كالتالي: ضغط مع خسارة بعض معلومات الصورة (Lossy): يؤدي استخدام هذا النوع إلى تصغير حجم الصورة تصغيرًا ملحوظًا، ولكن مع فقدان بعض معلومات الصورة. ضغط بدون خسارة بعض معلومات الصورة (Lossless): يؤدي استخدام هذا النوع إلى تصغير حجم الصورة تصغيرًا أقل من الطريقة السابقة، ولكن بدون فقدان بعض معلومات الصورة. إن طريقة الضغط الإفتراضية للحزمة imagemin للصور ذات النوعية JPEG هي الطريقة imagemin-jpegtran، سنستعرض كيفية ضغط الصور باستخدام الحزمة الأخرى وهي imagemin-mozjpeg. في البداية يجب علينا تثبيت هذه الحزمة من خلال الأمر التالي: npm install imagemin-mozjpeg ولنُعدل ملف الإعداد webpack.config.js لضبط طريقة ضغط الصور وفق ما نريده. ليصبح الملف على الشكل التالي: const imageminMozjpeg = require('imagemin-mozjpeg'); const ImageminPlugin = require('imagemin-webpack-plugin').default; const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require('path'); module.exports = { entry: './index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new CopyWebpackPlugin([{ from: 'img/**/**', to: path.resolve(__dirname, 'dist') }]), new ImageminPlugin({ pngquant: ({quality: [0.5, 0.5]}), plugins: [imageminMozjpeg({quality: 50})] }) ] } نلاحظ أن نسبة الضغط في هذه الحزمة من 100 إذ مررنا القيمة 50 وبذلك نخبره بأننا نريد ضغط الصور من النوع JPEG بنسبة 50%. ولنُنفذ الآن التعليمة التالية لنرى النتيجة: webpack --config webpack.config.js --mode production تهانينا في حال نفذّت جميع الخطوات تنفيذًا صحيحًا يجب أن تكون الصور مضغوطة وفقَ المطلوب. إن الأداة Webpack تحذرنا من الصور ذات الحجم الكبير، ولكنها لا تستطيع إخبارنا إن كانت الصور مضغوطة أم لا، ولهذا السبب سنستخدم الأداة Lighthouse للتحقق من التغييرات المُنفّذة. تسمح لنا الأداة Lighthouse من التحقق من ترميز الصور بكفاءة وإن كانت الصور الموجودة في صفحتك مضغوطة على نحوٍ أمثلي أم لا (لمزيد من المعلومات حول هذه الأدة ننصحك بالاطلاع على هذا المقال المفصّل). وستكون النتيجة مشابهة للصورة التالية: بنفس الطريقة يمكنك استخدام بقية الحزم لتخصيص قيمة الضغط المطلوب للأنواع الأخرى من الصور المستخدمة في مشروعك. إدارة الخطوط يمكننا إدارة الخطوط أيضًا مع Webpack من خلال مُحمل الملفات الّذي ثبتناه في إدارة الصور (سنعمل على نفس المشروع الّذي بنيناه في فقرة إدارة الصور)، والذي سيأخذ أي ملف تضعه له في ملف الإعداد ويضعه في المسار النهائي المُسند للخاصية (output) في ملف الإعداد webpack.config.js. أي أن مُحمل الملفات قادر على التعامل الخطوط أيضًا. لنعدل الآن ملف الإعداد webpack.config.js ليصبح على الشكل التالي: const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ], }, { test: /\.(png|svg|jpg|gif)$/, use: [ 'file-loader', ], }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/, + use: [ + 'file-loader', + ], + }, ], }, }; أضف خطًا معينًا إلى مجلد المشروع لتصبح البنية الهرمية للمشروع على الشكل التالي: webpack-demo |- package.json |- webpack.config.js |- /dist |- bundle.js |- index.html |- /src + |- my-font.woff + |- my-font.woff2 |- icon.png |- style.css |- index.js |- /node_modules من خلال التعديلات الّتي أجريناها على ملف الإعداد لمُحمل الملفات يمكننا الآن جعل التعليمة الّتي تصرح بها عن المسارات للخطوط ‎@font-face إلى الشكل التالي url(...)‎. سيعاد توجيه هذه المسارات إلى المسار النهائي المُسند للخاصية (output) في ملف الإعداد webpack.config.js تمامًا كما تعامل مع الصور. إذا سيصبح ملف التنسيق src/style.css على الشكل التالي: + @font-face { + font-family: 'MyFont'; + src: url('./my-font.woff2') format('woff2'), + url('./my-font.woff') format('woff'); + font-weight: 600; + font-style: normal; + } .hello { color: red; + font-family: 'MyFont'; background: url('./icon.png'); } لنرى كيف سيتعامل Webpack مع الخطوط ولننفذ أمر البناء على الشكل التالي: npm run build ... Asset Size Chunks Chunk Names 5439466351d432b73fdb518c6ae9654a.woff2 19.5 KiB [emitted] 387c65cc923ad19790469cfb5b7cb583.woff 23.4 KiB [emitted] da4574bb234ddc4bb47cbe1ca4b20303.png 3.01 MiB [emitted] [big] bundle.js 77 KiB 0 [emitted] main Entrypoint main = bundle.js ... افتح ملف index.html وانظر إلى النص "Hello webpack" كيف تغير شكل الخط ليصبح مطابق لشكل الخط الجديد. أما الآن لننتقل إلى واحدٍ من أبرز استخدامات مجمع الحزم Webpack وهو تحويل الشيفرات البرمجية الخاصة بالإصدارات الحديثة من المواصفات القياسية للغة جافاسكربت مثل ES6 إلى ES5. تحويل الشيفرة البرمجية باستخدام Babel بعد أن تعرفنا كيف ندير جميع التبعيات الموجودة في شيفرات جافاسكربت البرمجية من خلال Webpack. لا بُدّ لنا من جعل هذه الشيفرة البرمجية تعمل على جميع المتصفحات وذلك لكي نضمن أنه مهما يكن المتصفح الّذي سيستخدمه العميل (أو المستخدم) ستظهر النتيجة المطلوبة تمامًا مثل ما نريده. عملية التحويل (Transpiling): هي عملية تغيير الشيفرة البرمجية من إصدار معين إلى إصدار أقدم وذلك لضمان عمل هذه الشيفرة البرمجية على المتصفحات كلها. مقتطف من المخطط التفصيلي لدعم المتصفحات للمميزات الإصدار ES5 والإصدار ES6 من لغة جافاسكربت. سننشئ مشروعًا بسيطًا لشرح طريقة استخدام هذا المُحول: mkdir webpack-demo2 cd webpack-demo2 npm init -y npm install webpack webpack-cli --save-dev ملاحظة: لن نتطرق للتفاصيل نظرًا لأنها شُرحت في بداية هذا الدليل. ستكون البنية الهرمية للمشروع على الشكل التالي: webpack-demo2 |- package.json + |- index.html + |- /src + |- index.js في البداية سنحتاج لتثبيت ثلاث حزم للتعامل مع المحول Babel وستكون على الشكل التالي: npm install babel-core babel-loader babel-preset-env --save-dev الحزمة babel-core: تحتوي على الشيفرة البرمجية للنواة الأساسية للمُحول Babel. الحزمة babel-preset-env: تحتوي على الشيفرة البرمجية الّتي ستمكن النواة الأساسية للمحول Babel من تحويل الشيفرة البرمجية للجافاسكربت ذات الإصدار 6ES إلى الإصدار ES5. الحزمة babel-loader: أو (مُحمل المحول) وهي الّتي ستستخدمها Webpack في تحميل المحول والتعامل معه في عملية التحزيم (Bundling). أما الآن سننشئ ملف babelrc. والذي سيطلب من المحول Babel أن يستخدم الحزمة babel-preset-env أثناء عملية التحويل وسيكون الملف babelrc. على الشكل التالي: +{ "presets": ["env"] } ومن ثم سننشئ ملف الإعداد webpack.config.js ونطلب من Webpack الإستعانة بالمحمل Babel أثناء تحزيمه. وسيكون ملف الإعداد webpack.config.js على الشكل التالي: + const path = require('path'); + module.exports = { + entry: { main: './src/index.js' }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'main.js' + }, + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader' + } + } + ] + } + }; سنضع الآن في ملف src/index.js شيفرة برمجية من الإصدار ES6، وليصبح على الشكل التالي: + const fn = () => "Arrow functions're Working!"; + alert(fn()); ومن ثم ننفذ الأمر التالي: npm run dev ومن ثم لنفتح ملف dist/main.js نلاحظ أن الدالة السهمية (Arrow function) تُحوّلُ إلى دالة عادية وذلك وفق الطريقة المتبعة في الإصدار ES5 من لغة جافاسكربت. وسيكون محتوى الملف dist/main.js على الشكل التالي: 'use strict'; var fn = function fn() { return "Arrow functions're Working!"; }; alert(fn()); وبذلك استطعنا استخدام مميزات الإصدار الحديث من لغة جافاسكربت ES6 وتحويله إلى إصدار أقدم ES5 لتفهمه المتصفحات القديمة. هل من المخاطرة تعلم Webpack؟ بعد أن تعرفنا سريعًا على Webpack يمكن لسائل أن يسأل ماذا لو أنني تعلمت العمل على Webpack وبعدها ظهرت أداة جديدة أفضل منها أو تغطي مجال أوسع في العمل؟ أليس ذلك هدرًا لوقتي ولجهدي؟ صراحة، الشيء الوحيد الّذي استطيع تأكيده لك هو أن عجلة التحديث في مجال الواجهات الأمامية سريع الدوران، وما إن تظهر مشكلة ما حتى يتسارع المطورون لحلها فمن الممكن أن تظهر أداة أقوى منها (وهذا لا اتوقعه أن يحدث قريبًا وخصيصًا مع الدعم الكبير الّتي تشهده هذه الأداة من الشركات العملاقة مثل فيسبوك) هذا وارد الحدوث، ولكن هذا الأمر لا يُلغي فكرة أهمية تعلمك العمل مع هذه الأداة إذ أن معظم الأدوات الّتي تظهر لحل مشكلة معينة تكون متشابهة في البنية وطريقة العمل مع بعض الإختلافات. أي إن تعلمك العمل مع هذه الأداة سيجهزك للعمل مع الأدوات الأخرى المستقبلية. ومن يعلم ربما تكون أنت صاحب فكرة الأداة الجديدة الّتي ستتفوق على Webpack! والسؤال الأخر الّذي من الممكن يخطر في أذهاننا أيضًا هل تستحق هذه الأداة الوقت المبذول لتعلمها؟ أو بتعبير أخر هل النتائج الّتي سأجنيها من استخدام هذه الأداة ستغطي على الوقت المبذول لتعلمها؟ في الحقيقة لا أبالغ أن قُلت أنه من الواجب عليك تعلمها فورًا وبدون تردد وخصيصًا إن كُنت تتخذُ من مهنة تطوير الواجهات الأمامية (أو مطور برمجيات عمومًا) مصدرًا أساسيًا للدخل في حياتك! لأنها ستعزز كثيرًا من إنتاجيتك وسرعتك وجودة العمل المُسلّم وهذا ما ستشعر به من أول شهر من استخدامك لها. في الحقيقة إن مجيئ Webpack بكل هذه المميزات وقابلية التخصيص والإمكانيات أطاح بجميع المنافسين لديها مثل Browserify، ليس ذلك فحسب بل تخطّت إمكانياتها حدود الفكرة التي أُنشأت من أجلها لتهدد أدواتٍ مساعدةٍ أخرى كأدوات البناء مثل Gulp (والتي تحدثنا عنها في مقالٍ سابق) إذ أنه يمكننا من خلال NPM Script أن نعوّض عمل Gulp ولكن هذا سيكون على حساب الصعوبة إذ أن العمل مع Webpack ليس بسهولة العمل مع Gulp. ننصحك بالانتقال بعد الانتهاء من هذا المقال إلى مقال، أيهما أفضل كأداة مساعدة Webpack أم Browserify مع Gulp؟ الذي يشرح الفروقات بين كل هذه الأدوات وحالات استخدامها وغيرها من التفاصيل المفيدة. بالإضافة إلى ذلك وفرت لنا Webpack إمكانية التكامل مع أدوات مساعدة أُخرى مثل أداة السقالة Yeoman (والتي تحدثنا عنها في مقالٍ سابق) وبذلك نحصل على فوائد كِلتا الأداتين ونختصر وقتنا اختصارًا ملحوظًا. وبذلك يمكننا القول بأن المرونة الموجودة لدى Webpack وملحقاتها الرائعة والدعم المتميز من مجتمعها ومستخدميها جعلها من أقوى الخيارات الموجودة في الساحة حاليًا. الخاتمة بدأنا هذا الدليل ونحن لا نعرف شيئًا عن Webpack، ثم حفرنا عميقًا في تاريخها لنفهم تمامًا ما هي الأسباب الّتي أدت لظهور أداةٍ قوية كهذه الأداة، ثم بدأنا بتجهز المتطلبات اللازمة لتشغيلها، وفصلّنا في جميع المفاهيم الأساسية لها، وتعلمنا كيفية استخدامها في إدارة ملفات التنسيق والصور والخطوط ، وكيفية استخدامها لضغط الصور قبل رفعها وتعلمنا طريقة استخدامها لتحويل الشيفرة البرمجية من إصدار حديث إلى إصدار أقدم منه. وأخيرًا ناقشنا أهمية تعلمها وفوائده. وختامًا إن هذا الدليل لم يغطي كافة المميزات والجوانب الموجودة في أداة Webpack وإنما فقط أردنا من هذا الدليل أن يلفت نظرك لأهمية هذه الأداة وقدراتها المتميزة في مساعدتك في العمل على المشاريع وخصوصًا الكبيرة منها. بل وجعلها يدك اليمنى في أي مشروع جديد وبالتأكيد إن كلّ هذه المعلومات لن تُشبع فضول المطوّر المحترف ولذلك يمكنك دومًا الاطلاع على التوثيق الرسمي للأداة لمزيد من المعلومات. المصادر التوثيق الرسمي لأداة Webpack. التوثيق الرسمي لمدير الحزم npm. المقال Using Imagemin with webpack لصاحبته Katie Hempenius. المقال How to configure SCSS modules for Webpack لصاحبه Jon Preece. المقال Brief history of JavaScript Modules لصاحبه SungTheCoder.
  2. إن تزايد تعقيد تطبيقات الوِب في وقتنا الحالي جعل من ضرورة زيادة قابلية تطبيقات الوِب للتوسعة والتطوير أمرًا في غاية الأهمية. وعلى الرغم من أن الحلول القديمة المخصصة لكتابة شيفرات جافاسكربت و jQuery كانت فعَالة وكافية إلى حدٍ ما، ولكن بناء تطبيق وِبٍ في وقتنا الحاضر يتطلب درجة كبير من الانضباط والمنهجية الرسمية في تطوير البرمجيات. وهذه بعض الأمثلة على الممارسات الجيدة في تطوير البرمجيات: استخدام اختبارات الوحدة (Unit tests) وذلك للتأكد من أن تعديلات ما على الشيفرة البرمجية لن يعطّل شيفرة أُخرى. استخدام عملية كشف الأخطاء المحتملة (Linting) لضمان كتابة شيفرة برمجية خالية من الأخطاء. استخدام طرق مختلفة للبنية الهيكلية للشيفرة البرمجية لكلّ من وضع التطوير ووضع النشر. بالإضافة إلى ذلك فإن معايير الوِب بشكلها الحالي طرحت تحديات جديدة لتطوير تطبيقات الوِب. على سبيل المثال صفحات الوِب الحالية تُنشئ الكثير من الطلبات غير المتزامنة والّذي بدوره يؤدي إلى خفض أداء تطبيق الوِب انخفاضًا كبيرًا بسبب معالجة طلبات ملفات التنسيق والجافاسكربت. إذ أن لكل طلبٍ من هذه الطلبات (حتى وإن كان لملف صغير الحجم) فإنه سيحتاج إلى ترويسة اتصال (Header) وكُلفة إنشاء اتصال (handshakes). وهذه المشكلة تحديدًا تُعالجُ من خلال تجميع الملفات معًا لتصبح في ملف واحد وبذلك سنستدعي ملف جافاسكربت وملف تنسيق بدلًا من المئات الملفات. من الشائع في وقتنا الحالي استخدام لغات ذات معالجة مُسبّقة (language preprocessors) مثل: SASS و JSX والّتي تُترجم من هذه اللغات إلى ملفات جافاسكربت وملفات تنسيقات. بالإضافة إلى ذلك شاع أيضًا استخدام المحوّل (Transpilers) وهو محوّل يغيّر الشيفرة البرمجية من إصدار معين للغة جافاسكربت إلى إصدار أقدم، وذلك لضمان عمل هذه الشيفرة البرمجية على جميع المتصفحات. مثل المحوّل Babel. إن هذه المهام (والّتي لا علاقة لها بالمنطق البرمجي لتطبيق الوِب بحد ذاته) تمثل عبئًا على على المطور. ولكن لحسن الحظ يوجد حلٌ لهذا الأمر وهو منفذ المهام (Task Runner) والّذي جاء لمساعدتنا في أتمتة هذه المهام حتى نستطيع أن نحسّن من بيئة التطوير مع التركيز على بناء المنطق البرمجي لتطبيق الوِب. بمجرد ضبط الإعدادات الخاصة بمنفذ المهام كلّ ما عليك فعله هو استدعاء أمرٍ على الطرفية وستُنفذ بعدها المهام المؤتمتة. سأستخدم الأداة Gulp كمنفذ للمهام وذلك لأنه مناسب للمبرمجين وسهل التعلّم ومفهوم (تحدثنا في مقال سابق كيفية استخدام هذه الأداة بكلّ التفاصيل). مقدمة سريعة لأداة Gulp تتألف واجهة برمجة التطبيقات (API) لهذه الأداة من أربع دوالّ: gulp.src gulp.dest gulp.task gulp.watch في المثال التالي نلاحظ أن المهمة my-first-task ستستخدم ثلاثة دوال من أصل الأربعة. gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) }); عند تنفيذ المهمة my-first-task ستصغّر أولًا جميع الملفات المطابقة للنمط ‎/public/js/**/*.js‏ ومن ثمّ ستُنقل إلى المجلد build. الجميل في التعليمة ‏pipe()‎. هي أنه يمكنك أخذ مجموعة من ملفات الإدخال وتمريرها عبر الأنبوب لتنفيذ بعض التحويلات المناسبة عليهم ومن ثمّ إعادة ملفات الخرج المطلوبة. لجعل الأمور أكثر توافقية. غالبًا ما تنفذ العمليات المطلوبة في الإنبوب (مثل: minify()‎) من خلال مكتبات مدير الحزم npm، ونتيجة ذلك من النادر جدًا في التطبيق الفعلي لهذه االعمليات أن نحتاج لكتابة تفاصيل هذه العمليات كتابةً يدوية إلا إذا أردت أن تُعيد تسمية الملفات في الأنبوب. من الجدير بالذكر أن Gulp يَستخدم المجاري (streams) الّتي توفرها node.js، وهذا يسمح بتمرير البيانات الّتي ستُعالَج عبر الأنابيب (pipes) وهذا ما تفعله الدالة ‎.pipe()‎؛ لشرحٍ تفصيليٍ عن المجاري في node.js، سأحيلك إلى هذه المقالة. الخطوة التالية لفهم Gulp هي فهم مصفوفة تبعيات المهام. gulp.task('my-second-task', ['lint', 'bundle'], function() { ... }); في هذا المثال إن المهمة my-second-task تُعيد نتيجة الدالة المجهولة (anonymous function)، وذلك بعد اكتمال مهمة lint ومهمة التجميع bundle. وبذلك يُسمح لنا بفصل الاهتمامات عن بعضها بعضًا كما يمكنك أيضًا إنشاء سلسلة من المهام الصغيرة بمسؤولية واحدة مثل تحويل LESS إلى CSS. وإنشاء مهمة رئيسية (Master Task) والّتي ستستدعي ببساطة جميع المهام الأخرى (الصغيرة) عبر مصفوفة من تبعيات المهام. وأخيرًا لدينا التعليمة gulp.watch والّذي يراقب التغييرات في ملف (أو ملفات) ما والمطابق لنمط مرّر لها، وما إن يحدث تغيير ما في هذا الملف حتى تُنفذّ سلسلة من المهام المحددة. gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) }) في المثال السابق أي تغيير في الملفات الّتي تطابق النمط /public/js/**/*.js سيؤدي إلى تشغيل مهمة كشف الأخطاء المحتملة lint وبعدها مهمة إعادة التحميل reload. إن الاستخدام الشائع للتعليمة gulp.watch هو تشغيل عمليات إعادة التحميل المباشر في المتصفح، وهي ميزة رائعة جدًا أثناء مرحلة التطوير ولن تستطيع العمل بدونها بمجرد أن تجربها. وإلى هنا نستطيع القول بأننا فهمنا كلّ ما سنحتاج لاستخدامه في الأداة Gulp. أين سنستخدم أداة Webpack؟ عندما نستخدام نمط CommonJS فإن تجميع كلّ ملفات الجافاسكربت ليصبحوا في ملف واحد ليس بهذه البساطة. إذ إن الخاصية (entry point) والّتي تُسند عادةً للقيمة index.js أو app.js مع سلسلة من التعليمات require أو import الموجودة في أعلى الملف. وسيكون شكل ملف جافاسكربت في الإصدار ES5 على الشكل التالي: var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2'); وسيكون شكل ملف جافاسكربت في الإصدار ES6 على الشكل التالي: import Component1 from './components/Component1'; import Component2 from './components/Component2'; إن المثالين السابقين يجلبان التبعيات قبل تنفيذ بقية الشيفرات البرمجية في ملف app.js، وممكن أن يكون لهذه التبعيات تبعيات أخرى والّتي ستُجلب أيضًا. وبالإضافة إلى ذلك من الممكن أن تُستدعى نفس التبعية في أماكن متعددة في تطبيق الوِب خاصتك. ولكننا نريد جلب هذه التبعية مرة واحدة فقط. لهذا فأن كانت شجرة التبعيات بعمق عدة مستويات (أي التبعيات ذات هرمية كبيرة) فعندها ستزداد صعوبة تجميع هذه التبعيات في ملف واحد. ولكن لحسن الحظ يوجد حلّ رائع لهذه المشكلة وهو مُجمّع الحزم (الوحدات) مثل:Browserify أو Webpack. لماذا يفضل المطورون استخدام Webpack بدلًا من Gulp؟ بما أن أداة Webpack (تحدثنا في مقال سابق عن كيفية استخدام مجمع الحزم Webpack وأشهر طرق استعمالها في المشاريع) لتجمّيع الحزم وأداة Gulp لتنفيذ المهام من الممكن أن نتوقع أن نرى استخدام هاتين الأداتين مع بعضهما بعضًا، ولكن هنالك توجّه عام نحو استخدام Webpack بدلًا من Gulp وخصيصًا في مجتمع مطوري React. ولكن لما هذا التوجه؟ ببساطة إن قوة أداة Webpack مكنتها من تنفيذ الغالبية العظمى من المهام الملقاة على عاتق منفذ المهام (مثل: Gulp أو أي منفذ مهام عمومًا). فعلى سبيل المثال توفر Webpack خيارات تصغير وخرائط الشيفرة البرمجية المحوّلة Source Maps (لمزيد من المعلومات عنها يمكنك الإطلاع على المقال التالي) للشيفرة البرمجية المُجمعّة. بالإضافة إلى ذلك يمكن استخدامها كوسيط (Middleware) من خلال خادم مخصص يدعى webpack-dev-server والّذي يدعم كلًا من إعادة التحميل المباشر (live reloading) وإعادة التحميل النشط (hot reloading) لصفحات الوِب (والّتي سنتحدث عنها لاحقًا في هذا المقال). وكما يمكنك أيضًا تحويل الشيفرة البرمجية (transpiling) من إصدار جافاسكربت حديث مثل ES6 إلى إصدارٍ قديم مثل: ES5. ويمكنك أيضًا استخدام طريقة المعالجات المُسبقة (pre-processors) أو المُلحقة (post-processors) لملفات التنسيق. وبذلك تُترك عملية إختبار الوحدة (Unit Tests) وعملية كشف الأخطاء المحتملة (linting) كمهام رئيسية مستقلة نظرًا من كوننا قلصنا ما لا يقلّ عن ستة مهام محتملة للأداة Gulp لمهمتين فقط. يلجأ العديد من المطورين لاستخدام NPM Scripts بدلًا من استخدام Gulp، وذلك لتجنب إضافة أداة Gulp إلى المشروع في حين وجود بديل قوي ينوب عنها. في الحقيقة إن السيئة الرئيسية لاستخدام Webpack هي صعوبة ضبط إعداداته، مما يجعله خيارًا غير مرغوب به في حال أردنا إنشاء مشروع وتشغيله بأسرع وقتٍ ممكن. طرق إعداد منفذ مهام سنتعرف على ثلاث طرق لإنشاء وإعداد منفذ مهام مساعد في المشاريع البرمجية. وكلّ واحدٍ منهم سينفذ المهام التالية: إعداد خادم تطوير مع ميزة إعادة التحميل المباشر (live reloading) فور حدوث أي تعديل على صفحة الوِب المُراقبة. تجميع ملفات التنسيق والجافاسكربت (بالإضافة إلى تحويل الشيفرة البرمجية للغة جافاسكربت من الإصدار ES6 إلى ES5، وتحويل ملفات التنسيق من SASS إلى ملفات CSS وخرائط الشيفرة البرمجية المحوّلة) وذلك بطريقة قابلة للتطوير والتوسّع لهذه الملفات المحوّلة. تشغيل اختبار الوحدة (Unit Tests) سواءً كمهمة قائمة بحد ذاتها أو في وضع المراقبة. تشغيل عملية الكشف عن الأخطاء المحتملة (Linting) سواءً كمهمة قائمة بحد ذاتها أو في وضع المراقبة. توفير القدرة على جمع كلّ المهام السابقة عبر أمر واحد لكتابته عبر الطرفية. وجود أمر آخر لتجميع الملفات وضغطها أو تنفيذ تحسينات أخرى عليها من أجل عملية النشر. وستكون طرق إعداد منفذ المهام على الشكل التالي: Gulp + Browserify Gulp + Webpack Webpack + NPM Scripts سنعتمد في تطبيقنا العملي على استخدام React للواجهات الأمامية. في الحقيقة أردت في البداية أن أعمل بدون إطار عمل، ولكن استخدام React سيُبسط مسؤوليات منفذ المهام، إذ سيلزمنا فعليًا وجود ملف HTML واحد فقط، بالإضافة إلى أن React يعمل بسلاسة مع نمط CommonJS ولذلك اعتمدت أخيرًا على استخدامه. سنعمد على توضيح جميع المزايا والعيوب الخاصة بكل طريقة إعداد، وذلك لمنحك المعلومات الكاملة والصورة الشاملة لاتخاذ قرارٍ صائبٍ يناسب احتياجات المشروع الّذي تعكف على تطويره. سننشئ مشروعًا على Git وسنضيف له ثلاثة تفريعات من أجل اختبار كلّ طريقة من الطرق git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup) لنناقش الآن كلّ تفريعة (طريقة) من هذه الفروع على حدة. ستكون البنية الهرمية لمجلد المشروع على الشكل التالي: - app - components - fonts - styles - index.html - index.js - index.test.js - routes.js سنتطرق لشرح أهم الملفات الموجودة في المشروع وذلك حرصًا على كمال المعلومة. سيكون الملف index.html على الشكل التالي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="bundle.css"> <title>Gulp Browserify Setup</title> </head> <body> <div id="app"></div> <script src="bundle.js"></script> </body> </html> نلاحظ أن هذا الملف بسيط جدًا إذ إنه يُحمّلُ تطبيق React في الوسم <div id="app"></div> ولن نستخدم إلا ملف واحد للتنسيقات وملف آخر للجافاسكربت، في الواقع في طريقة الإعداد هذه لن نستخدم حتى ملف التنسيق bundle.css. وسيكون الملف index.js على الشكل التالي: import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app')); نلاحظ أن هذا الملف سيكون بمثابة نقطة الدخول (entry point) لتطبيقنا. إذ إننا بصدد تحميل React Router في الوسم div مع السِمة app الّتي أشرنا لها في الملف السابق. وسيكون الملف routes.js على الشكل التالي: import React from 'react'; import {Route, IndexRoute} from 'react-router'; import App from './components/App'; import HomePage from './components/home/HomePage'; import AboutPage from './components/about/AboutPage'; import ContactPage from './components/contact/ContactPage'; export default ( <Route path="/" component={App}> <IndexRoute component={HomePage} /> <Route path="about" component={AboutPage} /> <Route path="contact" component={ContactPage} /> </Route> ); نلاحظ أن هذا الملف يحدد مسارات المشروع. وستكون المسارات / و ‎/about و ‎/contact مرتبطة مع المكونات HomePage و AboutPage و ContactPage على التتالي. وسيكون الملف index.test.js على الشكل التالي: import expect from 'expect'; describe('Array', () => { it('should return -1 when the value is not present', () => { expect(-1).toBe([1,2,3].indexOf(4)); }); it('should correctly filter elements in an array', () => { const arr = [1,2,3,4,5]; const newArr = arr.filter(el => el > 3); expect(newArr.length).toBe(2); }); it('should correctly map elements in an array', () => { const arr = [1,2,3,4,5]; const newArr = arr.map(el => el * 2); expect(newArr).toEqual([2,4,6,8,10]); }); }); describe('Object', () => { it('should convert an int to a string with the toString method', () => { const val = 5; const valToString = val.toString(); expect(val).toNotBe(valToString); expect(valToString).toBe('5'); }); it('should correctly test whether an object has a certain property', () => { const obj = {a: 1, b: 2}; expect(obj.hasOwnProperty('a')).toBe(true); expect(obj.hasOwnProperty('c')).toBe(false); }); }); describe('String', () => { it('should convert a string to lowercase', () => { const str = 'HELLO'; const strLower = str.toLowerCase(); expect(strLower).toBe('hello'); }); it('should convert a string to uppercase', () => { const str = 'hello'; const strLower = str.toUpperCase(); expect(strLower).toBe('HELLO'); }); it('should remove spaces from both ends of a string', () => { const str = ' hello there '; const strTrimmed = str.trim(); expect(strTrimmed).toBe('hello there'); }); it('should split the string into an array of strings based on the provided delimiter', () => { const str = 'she-sells-seashells-down-by-the-seashore'; const strSplit = str.split('-'); expect(Array.isArray(strSplit)).toBe(true); expect(strSplit.length).toBe(7); }); }); نلاحظ أن هذا الملف يحتوي على سلسلة من اختبارات الوحدة (Unit Tests) والّتي ستختبر سلوك الشيفرة الأصلية للجافاسكربت (Native JavaScript) في عملية النشر الفعلية للتطبيق يمكنك إنشاء اختبار لكل مكون React (اختبار واحد على الأقل لكل مكوّن يعالج حالة معينة) واختبار السلوك الخاص بإطار العمل React. وعلى أية حال يكفي أن يكون لديك اختبار وحدة بسيط والّذي نستطيع تشغيله في وضع المراقبة (watch mode عند تفعيل هذا الوضع تثبت ملفات مخصصة لمراقبة أي تغييرات تحصل في الملفات الأخرى وفي حال حصول أي تغييرات في الملفات سيعاد ترجمة الملفات للحصول على الخرج الجديد). وسيكون الملف components/App.js على الشكل التالي: import React, {PropTypes} from 'react'; import Header from './common/Header'; class App extends React.Component { render() { return ( <div> <Header /> {this.props.children} </div> ); } } App.propTypes = { children: PropTypes.object.isRequired }; export default App; يمكن اعتبار الملف السابق حاوية لجميع مكونات العرض الموجودة لدينا. إذ تحتوي كلّ صفحة لدينا على الترويسة <Header/> بالإضافة إلى this.props.children والّذي يقيّم لعرض محتواه في الصفحة نفسها. فعلى سبيل المثال إذا كان في المتصفح /contact سيقيّم إلى ContactPage. وسيكون الملف components/home/HomePage.js على الشكل التالي: import React from 'react'; import {Jumbotron, Grid, Row, Col, Panel} from 'react-bootstrap'; class HomePage extends React.Component { render() { return ( <div className="HomePage"> <Jumbotron> <Grid> <h1>Home Page</h1> </Grid> </Jumbotron> <Grid> <Row> <Col sm={6}> <Panel header="Panel 1"> <p>Content A</p> <p>Content B</p> </Panel> </Col> <Col sm={6}> <Panel header="Panel 2"> <p>Content A</p> <p>Content B</p> </Panel> </Col> </Row> </Grid> </div> ); } } export default HomePage; هذا الملف سيكون الصفحة الرئيسية المعروضة. استخدمت react-bootstrap نظرًا لأن نظام الشبكة (Grid) في إطار العمل Bootstrap ممتاز لإنشاء صفحات متجاوبة. وعند استخدامه استخدامًا صحيحًا سيُقّلل عدد استعلامات الوسائط (media queries) الّتي سنكتبها للأجهزة صغيرة الحجم تقليلًا كبيرًا. المكونات الأخرى المتبقية (مثل: Header و AboutPage و ContactPage) مبنية بطريقة مشابهة (باستخدام react-bootstrap بدون التلاعب بالحالة [state manipulation]). أما الآن لنتحدث أكثر عن ملفات التنسيق. منهجية الملفات التنسيق أسلوبي المفضل في تنسيق ملفات React هو امتلاك ملف تنسيق خاص لكلّ مكوّن من المكونات. إذ سيكون هذا التنسيق ضمن نطاق هذا المكون فقط. ستلاحظ أنه في كلّ مكون من المكونات هنالك وسم div ذو مستوى أعلى وسيكون أسم الصنف التابع له مطابق لأسم المكون نفسه. لذلك المكون HomePage.js سيكون مغلف بوسم له الشكل التالي: <div className="HomePage"> ... </div> وسيكون هنالك أيضًا ملف HomePage.scss والّذي سيكون مهيكل على الشكل التالي: @import '../../styles/variables'; .HomePage { // Content here } لماذا هذه المنهجية مفيدة للغاية؟ لأنها تنتج لنا وحدات (Modular) ذات جودة عالية. مما يُلغي الكثير من المشاكل غير المرغوب بها في التنسيق. لنفترض أن لدينا مكونين React وهما Component1 و Component2. وفي كلّ واحد منهما نريد إعادة تنسيق الوسم h2 ليصبح حجم الخط كما في الملف التالي: /* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } } إن حجم الخط المخصص للوسم h2 في المكونين Component1 و Component2 مكتوبان بمعزل عن بعضهما البعض سواءً أكان المكونين متجاورين أو متداخلين لن يؤثر أحدهما على الآخر. أي أن المكون سيبدو شكله مثل ما خُطط له تمامًا بغض النظر عن مكان وجوده. في الحقيقة إن الأمر ليس بهذه السهولة دائمًا، ولكنه وبكل تأكيد خطوة كبيرة باتجاه أفضل الممارسات الصحيحة للتنسيق. وبالنسبة للتنسيقات المُسبقة للمكونات (per-component styles) أحب أن أخصص مجلد تنسيقات styles يحتوي على جميع التنسيقات العامة في الملف global.scss بجانب تنسيقات SASS مخصصة والّتي تكون مسؤولة عن معالجة التنسيقات المخصّصة (في حالتنا مثل: ‎_fonts.scss و ‎_variables.scss من أجل الخطوط والمتغيّرات على التوالي). تتيح لنا هذه التنسيقات العامة تحديد الشكل العام للتطبيق بأكمله بينما يمكن للتنسيقات الأخرى المخصصة أن تُستورد بحسب الحاجة لها. والآن بعد أن استكشفنا بعمق الشيفرة البرمجية المشتركة بين كلِّ طرق إعداد منفذ المهام. لننتقل إلى أول طريقة منهم. 1. طريقة Gulp + Browserify يتكون ملف gulpfile.js من 22 سطرًا من تعليمات لاستيراد المكتبات والحزم و150 سطرًا من الشيفرات البرمجية الأخرى. لذا ومن أجل الإيجاز سنراجع بالتفاصيل أهم النقاط الرئيسية في هذا الملف (مثل: js و css و server و watch و default). محزم ملفات جافاسكربت // ضبط إعداد Browserify const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); } نلاحظ في الدالّة bundle أن ملفات الجافاسكربت ستُحزّم باستخدام Browserify وستُستخدم خرائط الشيفرة البرمجية المحوّلة (Source maps) في وضع التطوير بينما ستُستخدم عملية التصغير لملفات جافاسكربت في وضع الإنتاج. في الحقيقة إن هذا النهج سيء وذلك لعدة أسباب إحداها أن المهمة ستقسّم إلى ثلاثة أجزاء منفصلة. إذ في البداية سننشئ كائن للتحزيم وليكن b وسنمرر له بعض الخيارات المطلوبة، ومن ثمّ نحدد بعض مُعالِجات الأحداث (event handlers). ثم لدينا مهمة Gulp والّتي يجب أن نمرر لها اسم الدالة بدلًا من تمريرها سطريًا (إن b.on('update') تستخدم نفس الطريقة). إن هذه الطريقة غير أنيقة مثل الطريقة الموجودة في مهام Gulp والّتي تتطلب فقط تمرير gulp.src وتوجيه بعض التغييرات. هناك مشكلة أخرى تجبرنا على اتباع طرق مختلفة لإعادة تحميل ملفات html و css و js في المتصفح. لاحظ طريقة إعداد مهمة المراقب الموضحة في الشيفرة التالية: gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); }); عندما سيحصل أي تغيير في ملف html سيعاد تشغيل مهمة html من جديد. gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); }); إن آخر تعليمة في الأنبوب هي عملية إعادة التحميل المباشرlivereload() إذا كانت قيمة الخاصية NODE_ENV ليست production، ستُحدث الصفحة في المتصفح تحديثًا تلقائيًا. يتكرر نفس السيناريو من أجل مُراقب ملفات التنسيق. إذ عند حدوث أي تغيير في أحد ملفات التنسيق تُستدعى المهمة css ويعاد تشغيلها من جديد، وأخر تعليمة في الأنبوب تكون لاستدعاء التحميل المباشر في المتصفح وذلك لعرض التغييرات مباشرة. غير أن المراقب الخاص بملفات الجافاسكربت لا يستدعي مهمة js على الإطلاق، وإنما معالج الأحداث في الأداة Browserify يتعامل مع إعادة التحميل باستخدام منهجية مختلفة تمامًا (يطلق عليها اسم مبدل الوحدات النشط [hot module replacement]). إن التضارب في هذه المنهجية مزعج للغاية. ولكنه للأسف ضروري وإذا استدعينا تعليمة إعادة التحميل المباشر في نهاية دالة bundle فعندها سيؤدي ذلك إلى إعادة بناء كلّ ملفات جافاسكربت في حال حدث أي تغيير في أي ملف منهم. من الواضح أن هذه المنهجية غير قابلة للتوسّع والتطوّر. إذ كلما زاد عدد ملفات جافاسكربت كلما استغرقت عملية إعادة التجميع وقتًا أطول. وفجأة عملية البناء الّتي كانت تستغرق 500 ميلي ثانية ستستغرق 30 ثانية مما سيُعيق منهجية التطوير الرشيق (agile development). محزم ملفات التنسيق gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); }); إن المشكلة الأولى الّتي تظهر هنا هي تضمين المكتبات أو ملفات التنسيق. إذ يجب علينا أن نتذكر أن نغيّر اسم الملف في كلّ مرة يظهر بها إصدار جديد سواءً للمكتبة (أو لأي ملف تنسيق) وذلك بحذف اسم المكتبة القديمة واستبدالها بالجديدة إلى مصفوفة gulp.src؛ بدلًا من إضافة أمر التضمين في مكان مناسب أكثر في الشيفرة البرمجية. المشكلة الرئيسية الأخرى هي المنطق المعقد في كلّ أنبوب. إذ إنني اضطررت لإضافة مكتبة من مستودع مدير الحزم NPM تدعى gulp-cond فقط لإضافة الجمل الشرطية في تعليمات الأنبوب، والنتيجة النهائية لتعليمات الأنبوب ليس من السهل قراءتها (لأن الأقواس الثلاثية في كلّ مكان!). مهمة الخادم gulp.task('server', () => { nodemon({ script: 'server.js' }); }); هذه المهمة واضحة للغاية. إذ إنه في الأساس مُغلّف لأمر استدعاء nodemon server.js والّذي سيُشغّل ملف server.js في بيئة Node.js. استخدامنا nodemon بدلًا من node إذ ستؤدي أي تغييرات تحدث في الملف إلى إعادة تشغيل الخادم. افتراضيًا nodemon تُعيد تشغيل العملية عند أي تغيير في ملف الجافاسكربت ولهذا السبب من المهم لتضمين ملف nodemon.json للحدّ من مجاله. { "watch": "server.js" } لنراجع الآن الشيفرة البرمجية الخاصة بملف server.js const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express(); يؤدي هذا الإعداد إلى تعيين الدليل الأساسي للخادم والمنفذ (port) بناءً على بيئة Node.js. وينشئ نسخة من express. app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir))); ستضيف هذه التعليمات برمجية وسيطة (Middleware) وهي connect-livereload وذلك لأنها الضرورية لإعداد ميزة إعادة التحميل المباشر، كما ستضيف هذه التعليمات أيضًا برمجية وسيطة ساكنة (Static Middleware) والّتي ستتعامل مع الملحقات الثابتة. app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); }); إن الشيفرة البرمجية السابقة هي مجرد مسار بسيط لواجهة برمجة التطبيقات. فإذا انتقلت إلى المسار localhost:3000/api/sample-route في المتصفح سترى ما يلي: { website: "Toptal", blogPost: true } في الواجهات الخلفية للتطبيق سيكون لدينا مجلد كامل مخصص لمسارات الواجهة البرمجية للتطبيقات، وملفات منفصلة لإنشاء اتصالات مع قاعدة البيانات. هكذا أُضيف المسار وبهذه البساطة لإثبات أننا يمكننا بناء الواجهات الخلفية بعد بناء الواجهات الأمامية. app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); }); هذا المسار الشامل يعني أنه مهما يكن الرابط التشعبي الّذي تودّ الذهاب إليه من خلال المتصفح فسيعرض لك الخادم صفحة index.html الوحيدة، وبعدها تأتي مهمة موجّه المسارات في React لعرض الصفحة المناسبة الّتي طلبتها من جانب العميل بدلًا من جانب الخادم. app.listen(port, () => { open(`http://localhost:${port}`); }); الشيفرة البرمجية السابقة تُخبر نسخة express للتنصت على المنفذ الّذي خصصناه وفتح تبويب جديد في المتصفح لعنوان الرابط الّذي طلبتَ الوصول إليه. الشيء الوحيد الّذي لا يعجبني في طريقة إعداد الخادم هو: app.use(require('connect-livereload')({port: 35729})); نظرًا لأننا نستخدم بالفعل gulp-livereload في ملفنا gulpfile، مما يدل على وجود مكانين منفصلين وجب علينا استخدام إعادة التحميل المباشر. إعداد المهام الافتراضية gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); }); تنفذ المهمة السابقة من خلال كتابة الأمر gulp في الطرفية. الغريب في الأمر أنك تحتاج لاستخدام runSequence من أجل تنفيذ التعليمات تسلسليًا. عادة ما تنفذ المهام بالتوازي ولكن هذه الطريقة ليست مطلوبة دومًا. فعلى سبيل المثال نحتاج لتشغيل مهمة clean قبل مهمة html للتأكد من أن مجلدات الهدف فارغة قبل نقل الملفات إليها. عندما صدرت النسخة الرابعة من gulp 4 كان إحدى مميزاتها أنها دعمت تعليمتي gulp.series لتنفيذ التعليمات بالتسلسل و gulp.parallel لتنفيذ التعليمات بالتوازي. وفضلًا عن ذلك، إن عملية إنشاء واستضافة التطبيق من خلال أمرٍ واحد هو فعلًا أمرٌ رائع، وإمكانية فهم أي جزء من أجزاء سير العمل بسهولة مثل سهولة اختبار مهمة ما في تسلسل التنفيذ. بالإضافة إلى ذلك يمكننا تفكيك المهام الكبير إلى أجزاء أصغر لنهج أكثر دقة في إنشاء واستضافة التطبيق. على سبيل المثال يمكننا إعداد مهمتين منفصلتين لإجراء عمليات إختبار المهام الأخرى وكشف الأخطاء المحتملة (linting). أو يمكن أن يكون لدينا مهمة المضيف والّتي ستُشغل الخادم والمراقب. هذه القدرة على تنظيم المهام قوية جدًا، وخاصةً عندما يتغيّر تطبيقك ويتطلب المزيد من المهام المؤتمتة. بناء التطبيق وتخصيصه لوضع التطوير مقابل وضع النشر if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production'; من خلال استخدام مكتبة yargs الموجودة في مستودعات مدير الحزم NPM. يمكننا تزويد الأداة Gulp بالرايات (Flags). هنا مثلًا سنوجّه ملف gulpfile لتعيين بيئة Node.js في وضع النشر إذا مرّرنا الراية ‎--prod مع التعليمة في الطرفية. سيستخدم هذا المتغيّر PROD كشرط للتمييز بين وضع التطوير ووضع النشر في ملف gulpfile. على سبيل المثال أحد الخيارات الّتي نمررها لإعدادات الأداة Browserify هي: plugin: PROD ? [] : [hmr, watchify] إن التعليمة الشرطية في الشيفرة السابقة مفيدة جدًا لأنها توفر علينا كتابة ملف gulpfile منفصل لكل من وضع التطوير ووضع النشر، والّذي سيحتوي على الكثير من التعليمات المكررة. بدلًا من ذلك يمكننا تمرير gulp --prod لتشغيل وضعية النشر في تنفيذ المهام. أو تمرير gulp html --prod لتشغيل المهمة html فقط في وضع النشر. من جهة أخرى رأينا سابقًا كيف أن تمرير التعليمات الشرطية عبر الأنبوب مثل: .pipe(cond(!PROD, livereload())) ليست من السهل قراءتها. ولكن في نهاية المطاف أنها مسألة تفضيلات شخصية إذا ما كنت تريد استخدام المتغيير المنطقي (PROD) أو إنشاء ملفين منفصلين (gulpfile) للإعداد. لننتقل الآن إلى طريقة الإعداد الثانية، وهي عندما نبدل الأداة Browserify لتحل محلها الأداة Webpack. 2. طريقة Gulp + Webpack نلاحظ أن ملف الإعداد gulpfile انخفض حجمه انخفاضًا كبيرًا إذ يحتوي الآن على 12 سطرًا من تعليمات استيراد المكتبات و 99 سطرًا من الشيفرات البرمجية الأخرى. إذا فحصنا من المهمة الافتراضية: gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); }); يتطلب إعداد تطبيق الوب خاصتنا الآن خمسة مهام فقط، بدلًا من تسعة. وهو في الحقيقة تحسنٌ كبير. بالإضافة إلى ذلك ألغينا الحاجة لاستخدام ميزة إعادة التحميل المباشر livereload، كما أن مهمة المراقب أصبحت كالتالي: gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); }); هذا يعني أن المراقب لن يشغل سلوك إعادة بناء التطبيق من جديد. وكميزة إضافية لن نحتاج إلى نقل الملف index.html من app إلى dist أو build بعد الآن. وبالعودة إلى فكرة تقليل المهام، نلاحظ إن المهام المخصصة لكلٍ من ملفات html و css و js و fonts استبدلت بمهمة واحدة: gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); }); بكلّ بساطة شغّل مهمتي clean و html بالتسلسل، وبمجرد ما ينتهي تنفيذهُم إجلب نقطة الدخول الخاصة بتطبيق الوب خاصتنا ومرّره إلى الأنبوب من خلال الأداة Webpack ثم مررها إلى ملف الإعداد الخاص بالأداة Webpack وهو webpack.config.js وذلك لتهيئه وأرسال الحزمة المُجمّعة الناتجة إلى baseDir (إما dist أو build اعتمادًا على الملف الإعداد ل Node.js). يمكنك إلقاء نظرة على ملف الإعداد webpack.config.js الخاص بالأداة Webpack، لكننا لن نشرحه كله، وإنّما سنشرح الخصائص المهمة المسندة للكائن module.exports. devtool: PROD ? 'source-map' : 'eval-source-map', تعيّن هذه التعليمة نوع خرائط الشيفرة البرمجية المحوّلة (source maps) والّتي سيستخدمها Webpack. تدعم Webpack مجموعة من الخياراتٍ المتنوعة من خرائط الشيفرة البرمجية المحوّلة. ويوفر كلّ وخيارٍ منهم توازنًا مختلفًا في الأداء. فمنهم خرائط ذات تفاصيل كثيرة مقابل خرائط ذات تفاصيل قليلة ولكن إعادة بنائها سريع (وهو الوقت المستغرق لإعادة تجميع الملف بعد إجراء التغييرات). وبناءً عليه يمكننا استخدام خرائط ذات تفاصيل قليلة من أجل زيادة سرعة إعادة التحميل في وضع التطوير واستخدام خرائط ذات تفاصيل كثيرة في وضع النشر. entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ] هذه هي نقطة الدخول إلى مُجمّع الحزم. لاح المصفوفة المُمررة لنقطة الدخول، وهذا يدلّ على إمكانية استخدام نقاط دخول متعددة. في حالتنا نقطة الدخول هي الملف app/index.js، وكذلك نقطة الدخول الخاصة لإعداد إعادة التحميل النشط للوحدات (hot module reloading) output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, وهنا نحدد مجلد الخرج والّذي سيوضع فيه الملفات المترجمة والمجمّعة. إن أكثر خيارٍ مربكٍ هنا هو publicPath والّذي يعيّن عنوان الرابط التشعبي الّذي سيُجمّع فيه ويوضع على الخادم. لذلك على سبيل المثال إذا كان publicPath هو /public/assets عندها ستكون الحزمة المجمّعة في هذا المسار /public/assets/bundle.js على الخادم. devServer: { contentBase: PROD ? './build' : './app' } هذه التعليمة ستُعلِم الخادم بالمجلد الجذر الّذي ستستخدمه في مشروعك. إذا شعرت بالإرتباك في طريقة تعيين مجلد الخرج لوضع الملف المجمّع فما عليك سوى تذكر هذه الدلالات لكلّ مما يلي: path + filename: الموقع المحدد للحزمة المُجمّعة في الشيفرة البرمجية للمشروع. contentBase (مثل: ملف الجذر /) + publicPath: موقع الحزمة على الخادم. plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], هذه بعض الملحقات الوظيفية الّتي ستعزز وظائف Webpack بطريقة ما. فعلى سبيل المثال إن الملحق webpack.optimize.UglifyJsPlugin هو المسؤول عن تصغير ملفات جافاسكربت. loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ] هذه هي المُحمّلات والّتي تعالج بشكل أساسي الملفات الّتي تُضمّن بواسطة require()، وهي تشبه إلى حدٍ ما الأنابيب في Gulp والّتي تمكنك من ربط المُحمّلات مع بعضهم بعضًا. لنستكشف أحد المُحمّلات: {test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'} إن الخاصية test تُخبر الأداة Webpack أن الملفات الّتي يجب على المُحمّل التعامل معها يجب أن تحقق التعبير النمطي المُمرر لهذه الخاصية. في حالتنا تكون /\.scss$/. أما الخاصية loader فهي تُحدد المُحمّل الّذي يجب أن نستخدمه. هنا نحدد المُحملات style و css و resolve-url و sass والّتي ستُنفذ بترتيب عكسي. يجب علي أن أعترف بأنني لست ماهرًا في بناء جملة تحديد المُحملات loader3!loader2!loader1. ولكن متى يجب علينا قراءة شيفرة برمجية من اليمين إلى اليسار؟ عدا هذا السطر. على الرغم من ذلك تعدّ المُحمّلات ميزة قوية جدًا لمُجمّع الحزم Webpack. في الحقيقة يسمح لنا المُحمّل الّذي استخدمته للتو باستيراد ملفات SASS بداخل الشيفرة البرمجية للجافاسكربت. فعلى سبيل المثال يمكننا استيراد ملفات التنسيق المخصّصة العامة الّتي عملنا عليها في المشروع في ملف الّذي حددنا كنقطة دخول وهو index.js وسيكون شكله كما يلي: import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app')); وبطريقة مشابهة في مكوّن الترويسة يمكننا استيراد الملف import './Header.scss' المرتبط بهذا المكون، وهذا وبكل تأكيد ينطبق على كافة المكونات الأخرى في التطبيق. من وجهة نظري الشخصية أرى بأن هذا الأمر يعدُّ تغييرًا ثوريًا في عالم التطوير بلغة جافاسكربت. إذ لا داعي للقلق بشأن تجميع ملفات التنسيق وتصغيرها وحتى خرائط الشيفرة البرمجية المحوّلة (source maps) أيضًا، وذلك لأن مُحمّلنا يتعامل مع كلّ هذه الأمور عوضًا عنا. حتى إعادة التحميل النشط للوحدات (hot module reloading) ستعمل على ملفات التنسيق خاصتنا، ومن ثمّ فإن القدرة على التعامل مع تعليمات الاستيراد من داخل ملف الجافاسكربت سيجعل من عملية التطوير أبسط من الناحية المفاهيمية، وأكثر تناسقًا، وتقلل من تبديل السياق في التطوير (وذلك عند استخدام أدوات مساعدة مختلفة)، والمنطق البرمجي أسهل في التفكير. لإعطاء ملخص موجز حول كيفية عمل هذه الميّزة مع مُجمّع الحزم Webpack وكيف سيضع ملفات التنسيق في ملف جافاسكربت المُجمّع. في الحقيقة لا يقتصر الأمر على ملفات التنسيق وحسب بل يمكنه أيضًا فعل ذلك من أجل الصور والخطوط. {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} إن الشيفرة البرمجية السابقة توجّه المُحمّل إلى تضمين الصور والخطوط بداخل الملف المُجمّع إن كان حجمهم لا يتعدى 100 كيلوبايت، وإلا فسيعرضهم كملفات منفصلة، كما يمكننا في أي وقت تغيير حجم المسموح به، ليصبح مثلًا 10 كيلوبايت. هذا هو باختصار طريقة إعداد مُجمّع الحزم Webpack وفي الحقيقة إنه يتطلب قدرًا لا بأس به من الإعداد. ولكن فوائد استخدامه رائعة للغاية. على الرغم من أن مُجمّع الحزم Browserify لديه أيضًا ملحقاته الجميلة وتحويلاته ولكنه لا يستطيع أن ينافس محملات Webpack من ناحية الخصائص الوظيفية المُضافة. 3. طريقة Webpack + NPM Scripts في هذه الطريقة من الإعداد سنستخدم npm scripts مباشرة بدلًا من الاعتماد على Gulp لأتمتة مهامنا. سيكون ملف package.json على الشكل التالي: "scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" } لتشغيل طريقة البناء من أجل وضع التطوير أو وضع النشر يمكنك كتابة الأوامر npm start و npm run start:prod على التتالي. نلاحظ أن هذا الملف أفضل وبكل تأكيد من ملف الإعداد gulpfile الّذي بنيناه سابقًا في هذا المقال. إذ اختصرنا الكثير من الشيفرات البرمجية لتكون عدد التعليمات بدلًا من 150 أو 99 تعليمة أصبحت 19 تعليمة (12 تعليمة إذا استثنينا التعليمات الخاصة بوضع النشر لأن معظمها مشابهة للتعليمات في وضع التطوير الّتي في بيئة Node.js لتصبح في وضع النشر). إن العيب الوحيد هو أن التعليمات مبهمة إلى حدٍ ما بالموازنة مع التعليمات الخاصة للأداة Gulp وليست معبرة مثلها. فعلى سبيل المثال لا توجد (على حسب اطلاعي على الأقل) بوجود تعليمة واحدة تشغّل npm script لينفذ تعليمات معينة تنفيذًا متسلسلًا وتعليمات أخرى ليُنفذها تنفيذًا متوازيًا. وإنما تعليمة واحدة لكل منهما. ومع ذلك هنالك الكثير من الميّزات الكبيرة لهذا المنهج. بالإضافة إلى أنه يوجد حزمة مقابلة لكل مكتبة من المكتبات الّتي كنا نستخدمها في السابق مع أداة Gulp. فبدلًا من استخدام المكتبات الموجودة في Gulp مثل: gulp-eslint gulp-mocha gulp-nodemon etc يمكننا استخدام الحزم التالية في Webpack: eslint mocha nodemon etc نقلًا عن كوري هاوس في مقالته "لماذا تركت أداة Gulp و Grunt وتوجهت إلى NPM Scripts": يحدد كوري هاوس ثلاث مشاكل رئيسية وهي: مشكلة الاعتماد على منشئي الملحقات. مشكلة تنقيح الأخطاء المرهقة. مشكلة التوثيقات الرسمية المفككة والضعيفة. ومن وجهة نظري أتفق مع جميع هذه المشاكل. 1. مشكلة الاعتماد على منشئي الملحقات مثلًا عندما تُحدّث مكتبة مثل: eslint، ستحتاج المكتبة المرتبطة بها gulp-eslint إلى تحديث مناظر لكي يتوافقوا مع بعضهم بعضًا. فإذا فقد المشرف الاهتمام بالعمل على تعديل وصيانة المكتبة، فإن الإصدار المخصص لأداة gulp من هذه المكتبة لن يتزامن مع النسخة الأصلية للمكتبة. وينطبق نفس الشيء عند إنشاء مكتبة جديدة. فعلى سبيل المثال إذا قام شخص ما بإنشاء مكتبة "xyz" وأطلقها، واحتجت فجأة إلى نسخة مخصصة من المكتبة للعمل مع أداة Gulp مثل: gulp-xyz. بمعنى آخر، هذا النهج غير قابل للتوسّع. ومن الناحية المثالية، نريد نهجًا مثل Gulp ولكن بإمكانه استخدام المكتبات الأصلية مباشرة. 2. مشكلة تنقيح الأخطاء المرهقة على الرغم من أن المكتبات مثل gulp-plumber تساعد في تخفيف هذه المشكلة تخفيفًا كبيرًا، إلا أنه من المعروف أن الإبلاغ عن الأخطاء في gulp ليس مفيدًا جدًا. فإذا كان هناك تعليمة واحدة خاطئة في الأنبوب فإنها سترمي استثناءً غير قابل للمُعالجة، وعندما تلاحق المشكلة في المكدس ستظهر مشكلة تبدو غير مرتبطة تمامًا بالسبب الحقيقي للمشكلة الّتي ظهرت في الشيفرة البرمجية. لهذا السبب يمكن أن يجعل من علمية تنقيح الأخطاء كابوسًا في بعض الحالات. ومهما بحثت عن حلول لهذه المشاكل سواءً على محركات البحث مثل: Google أو موقع Stack Overflow فلن يساعدك هذا البحث فعليًا إذا كان الخطأ مبهمً أو مضللًا. 3. مشكلة الوثائق الرسمية المفككة والضعيفة في كثير من الأحيان ألاحظ أن أحد مكتبات gulp الصغيرة يكون توثيقها الرسمي محدودًا للغاية. أظن بأن هذا الأمر يرجعُ من أن المُنشئ عادة لهذه المكتبة يكون هدفه الأساسي استخداماته الخاصة. بالإضافة إلى ذلك من الشائع أن تنّظر إلى التوثيق الرسمي لكلّ من الملحقات الإضافية للأداة Gulp والمكتبة الأصلية (الّتي تعتمد عليها تلك الملحقات)، مما يعني الكثير من تبديل السياق وزيادة كمية القراءة إلى الضعف بسبب ذلك. الخاتمة يبدو لي أنه من الواضح جدًا أن Webpack أفضل من Browserify وأن NPM scripts أفضل من Gulp، على الرغم من أن كلّ خيارٍ منهم له فوائده وعيوبه. ومن المؤكد أيضًا أن تعابير وتعليمات Gulp أكثر مقروئية وملاءمة للاستخدام من NPM scripts، ولكنك ستدفع الثمن غاليًا في جميع عمليات التعقيد المضافة. قد لا تكون كلّ طريقة من طرق الإعداد السابقة مثالية لتطبيقك، ولكن إذا رغبت في تجنب عدد هائل من تبعيات التطوير وتجربة تنقيح الأخطاء المحبطة، فإن Webpack مع NPM scripts هي الطريقة المناسبة لك. آمل أن تجد هذه المقالة مفيدة في اختيار الأدوات المناسبة لمشروعك القادم. ترجمة -وبتصرف- للمقال Webpack or Browserify & Gulp: Which Is Better? لصاحبه Eric Grosse