دليل عملي لفهم محزم الوحدات webpack

تحدثنا في مقال سابق عن Webpack من الناحية النظرية، وشرحنا دوره وأهميته في حياة مطوري الويب هذه الأيام.

وكنت قد وعدتكم بأن أقوم بإعداد درس جديد من أجل شرح Webpack واكتشافه بطريقة عملية حتى تترسخ في أذهاننا المفاهيم التي رأيناها في الدرس السابق، وها أنا اليوم أفي بوعدي لكم :)

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

البداية

لننشئ مجلدا اسمه wepback-project ونقوم بالدخول إليه :

mkdir wepback-project
cd wepback-project

ثم لنقم بتهيئة ملف packages.json عن طريق هذا الأمر :

npm init

بعد إنشاء الملف packages.json سنقوم بتحميل Webpack 4، وهي آخر نسحة من Webpack لحدود كتابة هذه الأسطر، مع تحميل حزمة Webpack-cli لتنفيذ بعض أوامر Webpack انطلاقا من Terminal.

npm install webpack webpack-cli --save-dev

الآن، عند تنفيذ الأمر webpack  من نافذة Terminal فإنه ستظهر لنا رسالة الخطأ هذه :

webpack error

الرسالة باللون الأصفر هي فقط رسالة تحذيرية تعلمنا بأن WEBPACK يفترض مسبقا بأننا في وضع الإنتاج (Production) إذا لم نقل له العكس، وبالتالي فإنه قد يقوم بتنفيذ بعض المهام الخاصة بهذا الوضع مثل ضغط ملفات CSS و JS إلخ...

أما الرسالة الثانية باللون الأحمر فهي رسالة خطأ تقول لنا بأن WEBPACK يبحث عن مجلد اسمه src في مشروعنا ولكن لم يجده. فانطلاقا من النسخة الرابعة، أصبح Webpack يعتمد مقاربة zero configuration التي تمكننا من القيام بعدد من المهام الأساسية من دون إنشاء ملف الإعدادات webpack.config.js. لهذا يفترض هذا المحزم بأن هناك مجلد اسمه src وبداخله ملف index.js، ويمكننا طبعا تغيير هذه الإعدادات الإفتراضية عبر ملف webpack.config.js كما سنرى لاحقا، أو حتى من دونه إنطلاقا من الأوامر السطرية فقط :

webpack ./src/index.js --output ./dist/main.js

وضع التطوير

لنقم بإنشاء المجلد src وملف index.js بداخله.

وبما أننا في مرحلة التطوير، فسنطلب من Webpack أن يقوم بعمله بناء على هذا الوضع، والطريقة هي عبر إضافة العلم mode-- لأوامر ويب باك :

webpack --mode development

webpack success

بعد تنفيذ هذا الأمر، ستتم عملية التحزيم بنجاح لأن ويب باك وجد كل الظروف التي افترضها مسبقا. وستلاحظون بأنه قام بإنشاء مجلد جديد اسمه dist بداخله ملف جافاسكريبت main.js وبداخله كود الجافاسكريبت المحزم، غير مضغوط لأننا طلبنا من Webpack أن يعمل وفق إعدادات وضع التطوير Development.

ولجعل بيئة عملنا أكثر احترافية وعملية، سنقوم بتنفيذ أوامر Webpack من داخل سكريبتات npm التي نقوم بإضافتها داخل الملف package.json في منطقة scripts. إليكم الطريقة :

"scripts": {
"dev": "webpack --mode development"
}

لإعادة تنفيذ الأمر السابق webpack --mode development ، يكفي تنفيذ هذا الأمر :

npm run dev

بهذه الطريقة يمكننا تنفيذ أوامر npm مهما كانت معقدة، ومهما أظفنا أعلاما وخصائص جديدة لأوامر Webpack فإن أمر npm الذي نقوم بتنفيذه في نافذة Terminal يبقى كما هو. وحتى إذا قدر لمطور آخر أن يعمل على مشروعنا، فسيمكنه معرفة الأوامر التي يجب تنفيذها على المشروع فقط بفتح الملف package.json ومعاينة الأوامر داخل الخاصية {} scripts .

لإضافة وضع بناء جديد خاص بمرحلة الإنتاج، فيمكننا إنشاء سكريبت جديد اسمه مثلا build بنفس الكيفية التي أضفنا بها السكريبت dev.

"scripts": {
  "dev": "webpack --mode development",
  "build": "webpack --mode production"
}
npm run build

لكي لا نضطر لإعادة تنفيذ هذه الأوامر بعد كل عملية تعديل على الملف index.js، سنقوم بإضافة علم جديد (flag) اسمه watch-- لأوامر Webpack بهذه الكيفية :

"scripts": {
  "dev": "webpack --mode development --watch",
  "build": "webpack --mode production --watch"
}

لنبدأ العمل في الأمور الجادة

أحدث طريقة لكتابة أكواد الجافاسكريبت معروفة باسم ES6، ولكن مع الأسف لا تدعمها حتى الآن بشكل كامل كل المتصفحات الكبيرة، لذلك علينا تحويل أكواد جافاسكريبت ES6 إلى أكواد جافاسكريبت ES5 مدعومة من كافة المتصفحات. هذه العملية معروفة باسم Transpiling، ويعتبر Babel هو أشهر Transpiler لأكواد الجافاسكريبت في الوقت الحالي.

لنكتشف معا كيفية استخدام Babel مع Webpack :)

تحميل وإعداد بابل Babel

أولا علينا تحميل بابل من مستودع npm مع ملحقاته التي سوف نحتاجها.

npm install babel-core babel-loader babel-preset-env --save-dev

قمنا هنا بتحميل 3 حزم :

  1. babel-core : الحزمة التي تحتوي على الشفرة المصدرية لنواة بابل.
  2. babel-preset-env : الحزمة التي تمكن نواة بابل من تحويل أكواد جافاسكريبت ES6 إلى أكواد جافاسكريبت ES5.
  3. babel-loader : هذه الحزمة تمثل ال loader الذي يستخدمه webpack لكي يعمل بتوافقية كاملة مع Babel. يعني أن Webpack يستعين ببابل في عملية Transpiling قبل أن يقوم بعملية التجميع أو التحزيم ( *Bundling *).

[alert type="info" icon-size="normal"]كما شرحنا في المقال السابق، فإن Webpack يعتمد على ما يعرف ب Loaders لكي يتمكن من تحزيم ومعالجة مختلف أنواع الملفات، وليس فقط ملفات جافاسكريبت (صور، CSS، خطوط، JSX ،TypeScript إلخ ...). [/alert]

الآن بعد أن قمنا بتحميل الحزم الثلاث، سنقوم بإنشاء ملف اسمه babelrc. لكي نطلب من بابل أن يستخدم الإضافة babel-preset-env ( تعرف هذه الإضافات في بيئة "بابل" ب Presets ) أثناء عملية Transpiling. ونكتب بداخله ما يلي :

{
  "presets": ["env"]
}

جاء الوقت الآن لنطلب من "ويب باك" أن يستعين ب Babel، سنفعل ذلك عن طريق ملف webpack.config.js الذي ذكرناه أكثر من مرة.

إعداد "ويب باك"

نحن الآن مطالبين بالإستعانة بالملف webpack.config.js من أجل إضافة عدد من الإعدادات المتقدمة ل Webpack.

لنشئ هذا الملف الآن، ولنضع فيه المحتوى التالي :

const path = require('path');

module.exports = {
  entry: { main: './src/index.js' },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  }
};

هذا هو أبسط صورة يمكن أن يكون عليها الملف webpack.config.js، وهذا الكود بسيط حيث يطلب من ويب باك فقط أن يبدأ التحزيم من ملف index.js (يسمى Entry point) على أن يجمع الكود النهائي في ملف main.js (يسمى Output file) وهي كما رأينا سابقا الإعدادات الإفتراضية التي يمكن تغييرها كما شئنا.

لنطلب الآن من Webpack أن يستخدم الحزمة babel-loader لكي نتمكن من كتابة أكواد جافاسكريبت ES6 :

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'
        }
      }
    ]
  }
};

لاحظوا بأننا أضفنا المنطقة {} module، هناك سيتم إضافة وإعداد جميع ال Loaders التي نحتاجها مع webpack في مشروعنا. بداخل المصفوفة rules نضع هذه ال loaders على شكل كائنات من نوع JSON، وكل كائن يضم مجموعة من الخصائص أهمها :

  • test : هذه الخاصية عبارة عن RegEx وتحدد ل webpack أي نوع من الملفات سيقوم بإجراء هذا loader عليها.
  • exclude : الملفات التي نريد استبعادها رغم أنها تحقق ال RegEx المحدد في الخاصية test.
  • use : هذه الخاصية تخبر Webpack بال loaders التي عليه إجراؤها وتطبيقها على هذه الملفات. (babel-loader في حالتنا).

سنفتح الآن الملف index.js ونكتب فيه بعضا من كود جافاسكريبت ES6 :

// src/index.js

const fn = () => "Arrow functions're Working!";

alert(fn());

بعد تنفيذ الأمر npm run dev سنلاحظ بأن Webpack قام بعملية التجميع في الملف main.js مع تحويل الدالة السهمية (Arrow function) أعلاه إلى دالة تقليدية بطريقة جافاسكريبت ES5.

// dist/main.js

'use strict';

var fn = function fn() {
  return "Arrow functions're Working!";
};

alert(fn());

إذن أصبح بإمكاننا الآن كتابة أكواد جافاسكريبت الحديثة في مشروعنا، و Webpack بفضل ال babel-loader سيتولى مهمة تجميعها وتحويلها لأكواد جافاسكريبت ES5 المدعومة من معظم المتصفحات.

ماذا عن ملفات HTML ؟

نحن الآن لدينا ملف dist/main.js المحزم والجاهز للإستدعاء في صفحة الويب، ولكن أين هي هذه الصفحة ؟ لا تقلق، صديقي، سنقوم بإضافتها حالا.

لنقم بإنشاء ملف اسمه index.html داخل المجلد src ونضع فيه المحتوى التالي :

dist/index.html
<html>
  <head> </head>

  <body>
    <h1>مرحبا بكم في توتومينا :)</h1>
  </body>
</html>

هناك مشكلة صغيرة تواجهنا الآن :( عندما نقوم بتنفيذ الأمر npm run dev فإنه لا يتم إنشاء أي ملف HTML داخل المجلد dist، رغم أننا قمنا بإنشاء الملف index.html في المجلد src. من الواضح إذن بأن Webpack لا يستطيع لوحده القيام بهذه المهمة. سنقوم بمساعدته بواسطة إضافة جديدة ( *Plugin *) اسمها HTML Webpack Plugin.

تلاحظون كذلك أننا لم نقم باستدعاء أي ملف جافاسكريبت في الصفحة src/index.html :) سنترك هذه المهمة أيضا للإضافة HTML Webpack Plugin التي سنقوم بتثبيها في مشروعنا حالا ؛)

npm i --save-dev html-webpack-plugin

بعد تثبيت إضافتنا الجديدة، سنقوم بإعداد Webpack لكي يقوم باستخدامها. هيا بنا إلى الملف webpack.config.js :)

...

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: {
  ...
  },
  output: {
  ...
  },
  module: {
    rules: [
    ...
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      hash: true,
      template: './src/index.html',
      filename: 'index.html'
    })
  ]

};

في البداية قمنا بإنشاء متغير (كلاس) جديد HtmlWebpackPlugin انطلاقا من الحزمة html-webpack-plugin، ثم قمنا بإنشاء نموذج (Instance) من هذا الكلاس داخل المصفوفة plugins، وبعد ذلك قمنا بتمرير 3 معاملات :

  • hash : قمنا بتمرير true لهذا المعامل حتى يتم إضافة سلسلة حروف وأرقام عشوائية إلى نهاية اسم ملف الجافاسكريبت المحزم ( مثال:  main.js?3cbc8ec659d08ad396e1 ) عند استدعائه في الصفحة. يستخدم هذا ال Hash لأغراض تتعلق بالكاش Cache الخاص بالصفحة.
  • template : هذا المعامل يمثل مسار القالب الذي سيبنى عليه ملف HTML المُخرَج.
  • filename : اسم ملف HTMLالمُخرَج.

وهناك مجموعة من المعاملات الأخرى يمكنكم الإطلاع عليها في صفحة الإضافة على Github.

الآن، بعض تنفيذ npm run dev مرة أخرى سنرى بأن Webpack قام بإنشاء ملف index.html داخل المجلد dist مع استدعاء الملف main.js المحزم (Bundled).

dist/index.html
<html>
  <head> </head>

  <body>
    <h1>مرحبا بكم في توتومينا :)</h1>

    <script type="text/javascript" src="main.js?3cbc8ec659d08ad396e1"></script>
  </body>
</html>

لم ننتهي بعد! أين هو CSS ؟

لا يوجد تطبيق ويب من دون CSS، لنقم بإنشاء ملف style.css في المجلد src.

ثم لنقم باستدعائه من ملف index.js، نعم من الجافاسكريبت :D

// src/index.js

import './style.css';

const fn = () => "Arrow functions're Working!";
alert(fn());

Webpack لا يستطيع التعامل مع ملفات CSS، فهو دائما ينتظر ملفات JavaScript. لهذا عند تشغيله سيظهر لنا هذا الخطأ الجميل والذي يخبرنا بأنه علينا مساعدة Webpack في إيجاد حل للتعامل مع ملفات CSS وذلك بتثبيت ال Loader المناسب لهذه الحالة.

webpack css error

ال Loader الذي نحتاجه هنا موجود واسمه css-loader، لنقم بتثبيته الآن :

npm install --save-dev css-loader

هذا loader يمكن Webpack من استيراد أكواد CSS باستخدام import أو require، وهذه حدود دور css-loader، هذا الدور ضروري ولكنه غير كافي.

نحن الآن لدينا ال CSS المستورد بفضل css-loader، ولكن كيف نقوم باستدعاء هذا CSS في الصفحة ؟

لحسن الحظ هناك إضافة (Plugin) أخرى اسمها mini-css-extract-plugin وتمكن من استخلاص ال CSS المستورد ووضعه في ملف (أو ملفات) CSS منفصل.

لنقم بتثبيت هذه الإضافة :

npm install --save-dev mini-css-extract-plugin

بعد التثبيت، سنقوم بإعداد Webpack لكي يستعين بهذه الإضافة الجديدة :

webpack.config.js
...

const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: { ... },
  output: {
  ...
  },
  module: {
  rules: [
    ...
    ,{
      test: /\.css$/,
      use: [
        MiniCssExtractPlugin.loader,
        "css-loader"
      ]
    }
  ]
  },
  plugins: [
    ...,
    new MiniCssExtractPlugin({
      filename: "bundle.css"
    })
  ]
};

مثلما فعلنا مع إضافة HtmlWebpackPlugin، قمنا بإنشاء نموذج من الكلاس MiniCssExtractPlugin وأضفناه في المصفوعة plugins، ثم طلبنا منه أن يكون اسم ملف ال CSS المخرج bundle.css. ولا ننسى كذلك أننا أعطينا تعليمات لِ Webpack من داخل المصفوفة rules لكي يقوم بإجراء css-loader و MiniCssExtractPlugin.loader (هذا الأخير يمكن تعويضه ب style-loader بعد تثبيته) على جميع الملفات من نوع css.

ومن الآن، بعد تشغيل Webpack سيتم استخلاص ال css المستورد من داخل الجافاسكريبت ويضاف إلى ملف جديد اسمه bundle.css في المجلد dist، ثم يتم استدعائه تلقائيا في الملف dist/index.html :) إليكم الحالة النهائية لهذا الملف :

dist/index.html
<html>
  <head>
    <link href="bundle.css?a583e8290b8c96b0a276" rel="stylesheet" />
  </head>

  <body>
    <h1>مرحبا بكم في توتومينا :)</h1>
    <script type="text/javascript" src="main.js?a583e8290b8c96b0a276"></script>
  </body>
</html>

وفي الصورة التالية، تجدون البنية النهائية لمشروعنا :

folders tree

النهاية

هكذا تعلمنا اليوم كيفية إضافة Webpack إلى مشاريعنا والإستعانة بالمزايا العديدة التي يوفرها بفضل العدد الكبير من Loaders و Plugins التي تزخر بها هذه البيئة.

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

لا تبخلوا علي بآرائكم حول هذه التقنية الرائعة، كما أدعوكم لطرح استفساراتكم وأسئلتكم حول هذا الموضوع في صندوق التعليقات أسفله، سأسعد كثيرا بالتواصل معكم :D