Mohamed Lahlah

شهد مجال تطوير الوِب في السنوات الأخيرة تطورات هائلة على جميع المستويات والذي أدى إلى تحسنيات كبيرة على مستوى السرعة والجودة وقابلية الصيانة. ليس ذلك فحسب بل تعدى الأمر إلى إمكانية أتمتة بعض المهام الروتينية والتركيز على جودة العمل المُسلم ورفع الإنتاجية وذلك باستخدام أدوات مساعدة مثل Webpack، والتي تعدّ من أشهر الأدوات المساعدة بل إنها أصبحت عنصرًا أساسيًا في أي مشروع يُبنى بلغة جافاسكربت.

حدثت بعض هذه التطورات على صعيد تطوير الواجهات الخلفية (Back-end) مثل ظهور Node.js والّتي تستخدم استخدامًا كبيرًا في هذه الأيام، وبعضها الآخر كان في تطوير الواجهات الأمامية (front-end)، ومن أبرز هذه التطورات هي النقلة النوعية للغة جافاسكربت ودعمها للعديد من المميزات والخصائص، والتي أدت في نهاية المطاف لجعلها واحدةً من أبرز الخيارات القوية في برمجة الوِب. بل وحتى إنها احتلت المرتبة الأولى في ترتيب أشهر لغات البرمجة شعبية لعام 2019 وذلك بحسب إحصائية موقع Stackoverflow.

most_popular_technologies.png

وكما أعلنت شركة 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>

هذه الطريقة جيدة جدًا للبدء إذ لا يوجد أي ملفات خارجية أو تبعية ناشئة بين الملفات، ولكن هذه هي الطريقة المثالية لبناء شيفرة برمجية غير قابلة للصيانة وذلك بسبب المشاكل التالية:

  1. عدم إمكانية إعادة استخدام الشيفرة البرمجية: فإذا احتجنا إلى إضافة صفحة أخرى وهذه الصفحة تحتاج إلى بعض الوظائف المحققة في الشيفرة البرمجية الّتي كتبناها سابقًا في الملف السابق فيجب علينا حينها نسخ الشيفرة البرمجية المطلوبة ولصقها في الصفحة المراد تحقيق الوظائف فيها.

  2. عدم وجود ثبات في التبعية بين الشيفرة البرمجية: أنت مسؤول عن تنسيق التبعية بين الدوال المستخدمة على سبيل المثال إن كان هنالك دالة معينة تعتمد في وظيفتها على نتيجة دالة أخرى محققة قبلها فيجب علينا حينها الانتباه لهذا الأمر وعدم تغيير ترتيب تواجد الدوال في الشيفرة البرمجية.

  3. التضارب في المتغيرات العامة (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;

ولكن هذا الحل لم يكُ مثاليًا بل واجه المشاكل التالية:

  1. عدم وجود ثبات في التبعية بين الشيفرة البرمجية.
  2. التضارب في المتغيرات العامة مازالت موجودة.
  3. تحديث المكتبات أو الملفات المستدعاة سيكون يدويًا في حال ظهور نسخة جديدة للمكتبة (أو الملف).
  4. زيادة عدد طلبات 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 تستخدم هذه الطريقة؛ إذ أنها فقط تكشف عن متغير عام وهو $ وكل التوابع تُستدعى عن طريق هذا الكائن.

في الحقيقة إننا إلى هذه اللحظة لم نصل إلى الحل النهائي إذ أن الحل السابق لا يزال يعاني من بعض المشاكل مثل:

  1. عدم وجود ثبات في التبعية بين الشيفرة البرمجية: نلاحظ في ملف index.html الملفات لا تزال بحاجة إلى وضعها وفق ترتيب معين يحافظ على التبعية بين الملفات إذ أن الملف myApp.js يجب أن يأتي قبل أي ملف وملف main.js يجب أن يأتي بعد كلّ الملفات.
  2. التضارب في المتغيّرات العامة: صحيح أننا قللنا عدد المتغيّرات العامة إلى الواحد ولكنها لا تزال موجودة.
  3. تحديث المكتبات: إن تحديث المكتبات (أو التبعيات أو أي ملف عمومًا) المستدعاة سيكون يدويًا في حال ظهور نسخة جديدة للمكتبة أو للتبعية.
  4. زيادة عدد طلبات HTTP بين الخادم والعميل: وذلك بعد زيادة عدد الملفات المستدعاة في ملف index.html.

ظهور CommonJS

جرت نقاشات عديدة في عام 2009 حول إمكانية جلب لغة جافاسكربت إلى الواجهات الخلفية (استخدامها من جانب الخادم) وفعلًا كان ذلك على يد مهندسٍ شاب يعمل لدى شركة موزيلا ويُدعى كيفن دانجور (Kevin Dangoor).

قدمت CommonJS طريقة لتعريف الوحدات لحل مشكلة المجال في جافاسكربت من خلال التأكد من تنفيذ كلّ وحدة في فضاء الأسماء (namespace) الخاص بها وذلك بإجبار الوحدات على تصدير تلك المتغيّرات صراحةً (الذين نريد عرضهم واستخدامهم في المجال العام) وأيضًا تعريف تلك الوحدات المطلوبة للعمل بالشكل الصحيح مع بعضها بعضًا.

بالإضافة إلى ذلك قدمت CommonJS واجهة برمجية مخصصة للجافاسكربت هذه المميزات فتحت أبصار العالم لإمكانيات هذه اللغة العظيمة، وجذبت اهتمام كبير من المطورين والذي انعكس في سرعة تطورها وتأقلمها مع جميع التغيّرات والمشاكل الّتي واجهتها.

إن CommonJS ليست مكتبة جافاسكربت وإنما هي معايير تنظيمية لكتابة الشيفرة البرمجية (على سبيل المثال الطريقة الّتي خصصتها لاستيراد الوحدات استيرادً منظمًا) وهذه المعايير شبيه بالّتي تطرحها منظمة ECMA ‏(European Computer Manufacturers Association).

ولتحقيق التنظيم في طريقة استدعاء الوحدات تقدم لنا CommonJS الدوال والأغراض المساعدة لذلك وهي:

  1. دالة require()‎: والّتي تسمح لنا باستيراد وحدات معينة في المجال المحلي الّذي نكون فيه.
  2. كائن الوحدة 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 حلّت آخر مشكلتين متبقيتين مع نمط الوحدات وهما على الشكل التالي:

  1. عدم وجود ثبات في التبعية بين الشيفرة البرمجية.
  2. التضارب في المتغيرات العامة.

نحن فقط سنهتم بالتبعيات الموجودة من أجل كلّ وحدة (أو ملف) وبالتأكيد في هذه الحالة لا يوجد هناك أي تضارب في المتغيرات العامة.

مُحمل الوحدات 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 وذلك لأن هذه المكتبة تفترض بأن كلّ القيم الّتي سنُسندها لهذه الخاصية ستكون ملفات جافاسكربت.

وسيكون تحميل التبعيات للمثال السابق كما في الصورة التالية:

dependencies.png

نلاحظ أن المتصفح يحمّل الملف index.html والملف بدوره يحمّل المكتبة require.js والّذي سيتكفل ببقية الملفات الأخرى.

وبهذا نكون حللنا جميع المشاكل الّتي واجهتنا حتى هذه اللحظة، ولكن وكما هي العادة في عالم البرمجة في كلّ محاولة لإصلاح مشكلةٍ ما تظهر مشكلةٌ أخرى، ولكن عادة ما تكون أخف من سابقتها في التأثير. ومن المشاكل الّتي ظهرت مع هذا الحل هي:

  1. صياغة AMD طويلة ومضجرة: إذ أن كلّ شيء يجب تغليفة بالدالة define ولذا يوجد مسافة بادئة (يُقصد تعريف الدالة define) للشيفرة البرمجية وهذه المشكلة يمكن ألا تظهر مع الملفات الصغيرة ولكن ما إن يكبر حجم الملف وتزداد تفاصيله ستصبح مشكلة كبيرة.
  2. قوائم الاعتماديات الموجودة في المصفوفة: إن قائمة الاعتماديات الموجودة في المصفوفة يجب أن تتشابه تمامًا مع القائمة المررة كوسيط للدالة وإذا كان لدينا العديد من التبعيات ستزداد الأمور تعقيدًا.
  3. في النسخة الحالية من 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 وهي تحول شجرة تبعية الملفات وتُجمعها في ملف واحد. ليس ذلك فحسب وإنما تحوي على الكثير من المميزات الأخرى نذكر منها:

  1. تقسيم الشيفرة البرمجية: عندما يكون لدينا تطبيقات متعددة تتشارك نفس الوحدات يمكننا من خلال هذه الأداة تجميع الشيفرة البرمجية في ملفين أو أكثر. فمثلًا إذا كان لدينا التطبيقين التاليين app1 و app2 ويشترك كلاهما في العديد من الوحدات إذا استخدمنا Browserify سيكون لدينا app1.js و app2.js وكل ملف منهم مجمع فيه الشيفرات البرمجية الخاصة بكل تطبيق، بينما مع أداة Webpack يمكننا إنشاء ملف app1.js وملف app2.js وبالإضافة إلى الملف المشترك shared-lib.js. نعم ستضطر إلى تحميل ملفين في صفحة Html ولكن مع إمكانية استخدام تقنيات مساعدة مثل: التخزين في الذاكرة المؤقتة للمتصفح (Cache) واستخدام شبكة توصيل المحتوى الموزعة CDN ‏(Content Delivery Network) كلّ هذه التقنيات ستقلل من وقت التحميل الإبتدائي للصفحات.
  2. المُحملات (Loaders): مع استخدام المُحمل المخصص يمكنك تحميل أي ملف إلى التطبيق إذ يمكنك استخدام الدالة reuiqre ليس فقط لتحميل ملفات جافاسكربت فقط (مثلما كانت أداة Browserify) وإنما ملفات التنسيقات وملفات SaSS وملفات Less بالإضافة إلى إمكانية تحميل الصور والخطوط والملفات والكثير غيرها.
  3. الملحقات (Plugins): تقدم Webpack ميزة الملحقات مجموعة واسعة من المهام مثل تحسين الحزمة وإدارة الملحقات (مثل الصور) بالإضافة إلى ذلك يمكننا حتى التلاعب بالملفات قبل تحميلها وتحزيمها في الملف الهدف (على سبيل المثال إمكانية ضغط الصور قبل رفعها على المخدم سنتطرق لهذه الميزّة لاحقًا في هذا الدليل).
  4. نظام الأوضاع (Mode): توفر لك هذه الأداة ثلاث أنواع من الأوضاع وضع التطوير ووضع الإنتاج أو بدون وضع. باختصار يستخدم كلّ وضع من الأوضاع من أجل سيناريو معين:
    • وضع التطوير: يستخدم عند العمل في مرحلة التطوير لمشروع الوِب وتكون الشيفرة البرمجية مقروءة ومفهومة.
    • وضع الإنتاج: يستخدم عند العمل في مرحلة عرض مشروع الوِب على الإنترنت وتكون الشيفرة البرمجية مختصرة وغير مقروءة.
    • بدون وضع: يكون الشيفرة الّتي استخدمت في مرحلة التطوير هي ذاتها الّتي ستعرض على الإنترنت بدون أي تعديل.
  5. تسهل عملية تحويل الشيفرات البرمجية إلى إصدار أقدم (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 على نظام لينكس):

  1. ندخل إلى الموقع الرسمي الخاص ببيئة Node.js ونحمل النسخة الموافقة لنظام التشغيل الخاص بك (يفضل تحميل الإصدارات ذات الدعم الطويل والتي تُدعى LTS).
  2. بمجرد إنتهاء التنزيل نفتح الملف المُنزّل.
  3. سيسألك معالج التثبيت عن موقع التثبيت والمكونات الّتي تريد تضمينها في عملية التثبيت يمكنك ترك تلك الخيارات بوضعها الافتراضي.
  4. انقر فوق تثبيت وانتظر حتى ينتهي معالج التثبيت.

تهانينا أصبحت بيئة Node.js جاهزة للعمل.

ملاحظة: يمكنك التأكد من تثبيت البيئة تثبيتًا صحيحًا من خلال تنفيذ الأمر التالي على سطر الأوامر (Command line)

node –v

يجب أن يظهر لك نسخة Node.js المثبتة وفي حالتنا ستظهر 12.16.1. كما يمكنك التأكد من تثبيت مدير الحزم Node Package Manager والّذي يُعرف اختصارًا npm من خلال الأمر

npm –v

يجب أن تظهر لك نسخة npm المثبتة وفي حالتنا ستظهر 6.13.4.

نحن جاهزون الآن لإنشاء مشروع جديد وتثبيت Webpack!

إنشاء مشروع جديد وتثبيت Webpack

  1. في البداية ننشئ مجلدًا جديدًا للمشروع وندخل إليه عبر التعليمات التالية:
$ mkdir project; cd project
  1. ثم ننشئ مشروعًا جديدًا عبر التعليمة التالية:
$ npm init -y
  1. ثم نثبت أداة Webpack عبر التعليمة التالية:
$ npm install -D webpack webpack-dev-server

يجب أن يظهر لك نتيجة مماثلة للصورة التالية:

webpack-installed.png

الآن لننشئ بنية الملفات الهرمية التالية:

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 لم يُصرح بوضوح عن حاجته لهذه المكتبة وإنما افترض أنها موجودة ضمن المتحولات العامة.

في الحقيقة إن إدارة المشروع بهذه الطريقة تعدّ مشكلة وذلك للأسباب التالية:

  1. ليس من الواضح أن الشيفرة البرمجية للملف index.js تعتمد على مكتبة خارجية.
  2. إذا كانت التبعية مفقودة، أو ضُمنت في الترتيب الخطأ، فلن يعمل التطبيق بالطريقة الصحيحة.
  3. إذا ضُمنت تبعيةً ما ولكن لم تُستخدم، سيضطر المتصفح إلى تنزيلها مع أنها غير ضرورية.

لنستخدم 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'

واخيرًا سنحصل على الخرج التالي:

webpack-4-css-modules.png

نلاحظ أن استخدام وحدات 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  

نلاحظ أن لدينا نوعين من طرق ضغط الصور وهما كالتالي:

  1. ضغط مع خسارة بعض معلومات الصورة (Lossy): يؤدي استخدام هذا النوع إلى تصغير حجم الصورة تصغيرًا ملحوظًا، ولكن مع فقدان بعض معلومات الصورة.
  2. ضغط بدون خسارة بعض معلومات الصورة (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 من التحقق من ترميز الصور بكفاءة وإن كانت الصور الموجودة في صفحتك مضغوطة على نحوٍ أمثلي أم لا (لمزيد من المعلومات حول هذه الأدة ننصحك بالاطلاع على هذا المقال المفصّل). وستكون النتيجة مشابهة للصورة التالية:

lighthouse_passing.png

بنفس الطريقة يمكنك استخدام بقية الحزم لتخصيص قيمة الضغط المطلوب للأنواع الأخرى من الصور المستخدمة في مشروعك.

إدارة الخطوط

يمكننا إدارة الخطوط أيضًا مع 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_and_ES6_support.png

مقتطف من المخطط التفصيلي لدعم المتصفحات للمميزات الإصدار 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 وإنما فقط أردنا من هذا الدليل أن يلفت نظرك لأهمية هذه الأداة وقدراتها المتميزة في مساعدتك في العمل على المشاريع وخصوصًا الكبيرة منها. بل وجعلها يدك اليمنى في أي مشروع جديد وبالتأكيد إن كلّ هذه المعلومات لن تُشبع فضول المطوّر المحترف ولذلك يمكنك دومًا الاطلاع على التوثيق الرسمي للأداة لمزيد من المعلومات.

المصادر



1 شخص أعجب بهذا


تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن