بناء إعدادات Webpack 4 احترافية من الصفر: دليل شامل لتحسين الأداء
Webpack هو أداة تجميع (bundler) ومدير تبعيات (dependency manager) قوية، تعتمد عليها العديد من الشركات الكبرى كجزء أساسي من أدوات تطوير الواجهة الأمامية (front-end). عادةً ما يتم إعداد Webpack عند بدء المشروع لأول مرة، ثم تُجرى تعديلات طفيفة على ملفات الإعداد (config files) حسب الحاجة. ولهذا السبب، قد لا يمتلك العديد من المطورين خبرة واسعة في التعامل مع Webpack.
في هذا الدليل العملي الشامل، سنستعرض أساسيات إعداد ملف Webpack خاص بك، جاهز للإنتاج (production-ready)، باستخدام الإصدار 4 من Webpack. سنتعمق في مناقشة إدارة المخرجات (output management)، وإدارة الأصول (asset management)، وإعدادات التطوير والإنتاج (dev and prod configs)، وBabel، وتصغير الكود (minification)، وتجاوز ذاكرة التخزين المؤقت (cache busting)، والمزيد.

التطبيق التجريبي: نقطة الانطلاق
لأغراض هذا العرض التوضيحي، سنقوم بإعداد ملف Webpack config من الصفر باستخدام Webpack 4. سيعتمد تطبيقنا على Vanilla JavaScript لتجنب التعقيدات المتعلقة بأي إطار عمل (framework) محدد. سيكون كود التطبيق الفعلي صغيراً نسبياً ليتسنى لنا التركيز بشكل أكبر على Webpack نفسه.
إذا كنت ترغب في متابعة الخطوات عملياً، يمكن العثور على جميع الأكواد المستخدمة في هذا المقال على GitHub.
هيكلة المشروع الأولية
للبدء، سنبدأ ببضعة ملفات فقط في دليل المشروع (project directory) الخاص بنا. تبدو بنية الدليل على النحو التالي:
webpack-demo
|_ src
|_ index.js
|_ .gitignore
|_ index.html
|_ package.json
|_ README.md
|_ yarn.lock
ملف index.html بسيط للغاية، يحتوي فقط على عنوان للصفحة وعلامة script:
<!doctype html >
< html >
< head >
< title > Webpack Training 1 </ title >
</ head >
< body >
< h1 > Webpack Training 1 </ h1 >
< script src = "./src/index.js" > </ script >
</ body >
</ html >
تشير علامة script إلى ملف ./src/index.js الخاص بنا، والذي يحتوي على بضعة أسطر من JavaScript تُخرج النص “Hello from webpack!”:
const p = document .createElement( 'p' )
p.textContent = 'Hello from webpack!'
document .body.append(p)
إذا قمت بسحب ملف index.html إلى متصفحك، يجب أن تكون قادراً على عرض صفحة الويب البسيطة الخاصة بنا:

تثبيت التبعيات الأساسية
لقد قمت بتضمين webpack و webpack-cli كـ devDependencies (تبعيات التطوير) في ملف package.json. لتثبيتها، قم بتشغيل الأمر التالي:
yarn install
اختبار تشغيل Webpack
يُعد Webpack 4 أداة “بدون إعدادات” (zero config) افتراضياً، مما يعني أنه يمكنك تشغيلها مباشرةً دون الحاجة إلى أي تهيئة أولية. بالطبع، لأي مشروع حقيقي ستحتاج إلى بعض الإعدادات، ولكن من الجيد إجراء فحص سريع للتأكد من أن Webpack يعمل دون الحاجة إلى المرور بالكثير من خطوات التهيئة الأولية.
لنجرب ذلك، قم بتشغيل:
yarn webpack
يجب أن ترى الآن دليلاً باسم dist تم إنشاؤه في دليل مشروعك. وداخله، يجب أن تجد ملف main.js، وهو الكود المصغّر (minified code) الخاص بنا. ممتاز! يبدو أن Webpack يعمل بشكل صحيح.
ربط ملف الإخراج بالصفحة
الآن بعد أن أصبح لدينا كود JavaScript في دليل dist، دعنا نجعل ملف index.html يشير إليه. بدلاً من أن تبدو علامة script هكذا:
< script src = "./src/index.js" > </ script >
دعنا نغيرها إلى هذا:
< script src = "./dist/main.js" > </ script >
الآن، قم بتحديث الصفحة في متصفحك. يجب أن ترى نفس المخرجات تماماً، ولكن هذه المرة يتم إنشاء النص “Hello from webpack!” بواسطة ملف ./dist/main.js.

إنشاء ملف إعدادات Webpack مخصص
بعد تثبيت Webpack وإجراء فحص سريع للتأكد من عمله، حان الوقت لإنشاء ملف إعدادات Webpack حقيقي. قم بإنشاء ملف باسم webpack.config.js وضع الكود التالي بداخله:
const path = require ( 'path' )
module .exports = {
entry : './src/index.js' ,
output : {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
}
}
- تخبر خاصية
entryأداةWebpackبموقع كود المصدر (source code) الخاص بنا. إنها “نقطة الدخول” (entry point) لتطبيقنا. - تخبر خاصية
outputأداةWebpackبالاسم الذي يجب أن يُطلق على ملف الإخراج (output file) والدليل الذي يجب وضعه فيه.
الأمر بسيط بما يكفي، أليس كذلك؟ الآن دعنا ننشئ سكربت npm في ملف package.json الخاص بنا:
"scripts" : {
"build" : "webpack --config=webpack.config.js"
}
الآن يمكننا تشغيل عملية البناء (build process) باستخدام الأمر yarn build. امضِ قدماً وقم بتشغيل هذا الأمر للتحقق من أن كل شيء تم إعداده بشكل صحيح. يمكنك حتى حذف دليل dist قبل تشغيل الأمر yarn build للتحقق من أن الدليل يتم إنشاؤه.
تغيير اسم ملف الإخراج
الآن، للمتعة فقط، دعنا نغير اسم ملف الإخراج. للقيام بذلك، سنفتح ملف webpack.config.js ونغير خاصية output من هذا:
output: {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
}
إلى هذا:
output: {
filename : 'tacos.js' ,
path : path.resolve(__dirname, 'dist' )
}
الآن قم بتشغيل yarn build مرة أخرى لإنشاء المخرجات. يجب أن ترى ملف tacos.js في دليل dist الخاص بك الآن. ولكن انتظر! نرى أيضاً ملف main.js القديم في دليل dist! ألن يكون رائعاً لو تمكن Webpack من حذف المخرجات القديمة غير الضرورية في كل مرة نقوم فيها ببناء جديد؟ لا بد أن هناك إضافة (plugin) لذلك.
إضافات Webpack (Plugins): تعزيز عملية البناء
يمتلك Webpack نظاماً بيئياً غنياً من الوحدات النمطية (modules) التي تُعرف باسم “الإضافات” (plugins). هذه الإضافات هي مكتبات (libraries) يمكنها تعديل عملية بناء Webpack وتحسينها. سنستكشف مجموعة من الإضافات المفيدة بينما نواصل تحسين إعدادات Webpack الخاصة بنا خلال بقية هذا المقال.

إضافة CleanWebpackPlugin: تنظيف دليل المخرجات
حسناً، بالعودة إلى مشكلتنا. سيكون من الرائع لو تمكنا من تنظيف دليل dist قبل كل عملية بناء جديدة. هناك إضافة لهذا الغرض! يمكننا استخدام CleanWebpackPlugin لمساعدتنا هنا.
أولاً، نحتاج إلى تثبيتها في مشروعنا:
yarn add --dev clean-webpack-plugin
لاستخدامها، سنقوم ببساطة باستيراد (require) الإضافة في ملف webpack.config.js الخاص بنا، ثم تضمينها في مصفوفة plugins ضمن إعداداتنا:
const path = require ( 'path' )
const { CleanWebpackPlugin } = require ( 'clean-webpack-plugin' )
module .exports = {
entry : './src/index.js' ,
output : {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
},
plugins : [
new CleanWebpackPlugin()
]
}
الآن قم بتشغيل yarn build مرة أخرى، ويجب أن ترى ملف إخراج واحداً فقط في دليل dist الخاص بك. تم حل المشكلة!

إضافة HtmlWebpackPlugin: إدارة ملفات HTML تلقائياً
من الأمور المزعجة قليلاً في إعداداتنا الحالية أنه في كل مرة نغير فيها اسم ملف الإخراج (output file name) في ملف webpack.config.js، يتعين علينا أيضاً تغيير اسم الملف الذي نشير إليه في علامة script ضمن ملف index.html. ألن يكون رائعاً لو تمكن Webpack من إدارة ذلك نيابة عنا؟ هناك إضافة لذلك! يمكننا استخدام HtmlWebpackPlugin لمساعدتنا في إدارة ملف HTML الخاص بنا.
دعنا نثبتها في مشروعنا الآن:
yarn add --dev html-webpack-plugin
الآن دعنا ننقل ملف index.html الخاص بنا داخل دليل src ليصبح مجاوراً لملف index.js.
webpack-demo
|_ src
|_ index.html
|_ index.js
|_ .gitignore
|_ package.json
|_ README.md
|_ yarn.lock
يمكننا أيضاً حذف علامة script في ملف index.html الخاص بنا، حيث سيتولى Webpack مهمة إدراج علامة script المناسبة لنا. احذف هذا السطر بحيث يبدو ملف index.html الخاص بك هكذا:
<!doctype html >
< html >
< head >
< title > Webpack Training 1 </ title >
</ head >
< body >
< h1 > Webpack Training 1 </ h1 >
</ body >
</ html >
الآن دعنا نستورد (require) هذه الإضافة في ملف webpack.config.js الخاص بنا، ثم نضمّنها في مصفوفة plugins ضمن إعداداتنا، تماماً كما فعلنا مع الإضافة الأولى:
const path = require ( 'path' )
const { CleanWebpackPlugin } = require ( 'clean-webpack-plugin' )
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
module .exports = {
entry : './src/index.js' ,
output : {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
},
plugins : [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename : 'index.html' ,
inject : true ,
template : path.resolve(__dirname, 'src' , 'index.html' ),
}),
]
}
في خيارات HtmlWebpackPlugin هذه، نحدد filename لاسم ملف الإخراج الذي نرغب به. نحدد لـ inject أننا نرغب في حقن ملف JavaScript الخاص بنا في علامة body عن طريق تعيين القيمة إلى true. وأخيراً، بالنسبة لـ template، نوفر موقع ملف index.html الخاص بنا في دليل src.

التحقق من السلامة (Sanity Check)
حسناً، دعنا نتأكد من أن كل شيء لا يزال يعمل بشكل صحيح. قم بتشغيل yarn build، وتحقق من أنك ترى ملفين في دليل dist الخاص بك: index.html و main.js. إذا نظرت عن كثب في ملف index.html، ستجد ملف main.js مشاراً إليه.
الآن، افتح ملف ./dist/index.html في متصفحك للتحقق من أن صفحتك تُحمّل بشكل صحيح. إذا اتبعت هذه الخطوات بدقة، يجب أن تظل صفحتك تعمل:

إنشاء خادم تطوير محلي (Development Server)
لقد حققنا تحسينات جيدة حتى الآن باستخدام CleanWebpackPlugin و HtmlWebpackPlugin. ولكن مع كل هذه التغييرات، كنا نضطر إلى تشغيل الأمر yarn build يدوياً في كل مرة لرؤية التحديثات الجديدة في تطبيقنا. كما أننا كنا نكتفي بعرض الملف مباشرةً في المتصفح بدلاً من عرض المحتوى المقدم من خادم يعمل محلياً.
دعنا نحسن من عملية التطوير لدينا بإنشاء خادم تطوير. للقيام بذلك، سنستخدم webpack-dev-server. أولاً، سنحتاج إلى تثبيته:
yarn add --dev webpack-dev-server
الآن، دعنا نقسم ملف webpack.config.js الوحيد الخاص بنا إلى ملفي إعدادات منفصلين: أحدهما للإنتاج (production) والآخر للتطوير (development). سنسمي ملف الإنتاج webpack.config.prod.js وملف التطوير webpack.config.dev.js.

إعدادات Webpack لبيئة التطوير
إليك ملف إعدادات التطوير الخاص بنا (webpack.config.dev.js):
const path = require ( 'path' )
const { CleanWebpackPlugin } = require ( 'clean-webpack-plugin' )
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
module .exports = {
mode : 'development' ,
devtool : 'inline-source-map' ,
devServer : {
contentBase : './dist' ,
},
entry : './src/index.js' ,
output : {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
},
plugins : [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename : 'index.html' ,
inject : true ,
template : path.resolve(__dirname, 'src' , 'index.html' ),
}),
]
}
لاحظ أننا حددنا mode كـ development الآن، وطلبنا inline-source-map لملفات JavaScript الخاصة بنا، مما يعني أن خريطة المصدر (source map) تُضمّن في نهاية كل ملف JavaScript. بالنسبة لخادم التطوير (dev server) الخاص بنا، حددنا أن المحتوى سيوجد في دليل dist. بقية إعدادات التطوير ظلت كما هي.
إعدادات Webpack لبيئة الإنتاج
الآن، إليك ملف إعدادات الإنتاج الخاص بنا (webpack.config.prod.js):
const path = require ( 'path' )
const { CleanWebpackPlugin } = require ( 'clean-webpack-plugin' )
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
module .exports = {
mode : 'production' ,
devtool : 'source-map' ,
entry : './src/index.js' ,
output : {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
},
plugins : [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename : 'index.html' ,
inject : true ,
template : path.resolve(__dirname, 'src' , 'index.html' ),
}),
]
}
هذا الملف أيضاً يبدو مشابهاً جداً لملف الإعدادات الأصلي. هنا، حددنا أن mode هو production، وأننا نرغب في خيار source-map لخرائط المصدر، والذي يوفر ملفات خرائط مصدر منفصلة للكود المصغّر.
سكربتات NPM لبيئتي الإنتاج والتطوير
أخيراً، دعنا نضيف المزيد من سكربتات npm في ملف package.json الخاص بنا حتى نتمكن من العمل مع إعدادات Webpack الخاصة بالتطوير والإنتاج:
"scripts" : {
"build" : "webpack --config=webpack.config.prod.js" ,
"build-dev" : "webpack --config=webpack.config.dev.js" ,
"start" : "webpack-dev-server --config=webpack.config.dev.js --open"
}
الآن، دعنا نجرب كل من هذه السكربتات:
- شغّل
yarn buildلرؤية مخرجات بناء الإنتاج (production build). يجب أن تلاحظ أن ملفmain.jsفي دليلdistالخاص بك مصغّر (minified) ولديه ملف خريطة مصدر (source map) مصاحب باسمmain.js.map. - الآن شغّل
yarn build-devلرؤية مخرجات بناء التطوير (development build). يجب أن ترى ملفmain.jsفي دليلdistالخاص بك، ولكن لاحظ الآن أنه غير مصغّر. - أخيراً، شغّل
yarn startلبدء خادم التطوير. سيؤدي هذا إلى فتح التطبيق علىhttp://localhost:8080/. لم يعد هناك حاجة لعرض الملفات مباشرةً عن طريق سحبها إلى متصفحك! لدينا الآن خادم تطوير حقيقي يعمل! يجب أن تبدو المخرجات التي تراها كما كانت دائماً:

إجراء التغييرات أثناء التطوير
الآن بعد أن أصبح لدينا خادم تطوير يعمل، دعنا نجرب إجراء بعض التغييرات البسيطة على ملف ./src/index.js الخاص بنا. بدلاً من إخراج “Hello from webpack!”، دعنا نغيرها لتقول “Hello from dev server!”. احفظ الملف، ثم شاهد الصفحة على خادم التطوير الخاص بك وهي تُعاد تحميلها وتُحدّث تلقائياً! سيمنح هذا دفعة رائعة لإنتاجيتك كمطور.

مبدأ “لا تكرر نفسك” (DRY)
الآن بعد أن أصبح لدينا ملفا إعدادات Webpack منفصلين، أحدهما للتطوير والآخر للإنتاج، ربما لاحظت وجود الكثير من الكود المكرر بين الملفين. كل مطور يعرف مبدأ DRY (Don't Repeat Yourself – لا تكرر نفسك) منذ اليوم الأول. إذا وجدت نفسك تكتب نفس الكود في أماكن متعددة، فقد يكون من الجيد تحويله إلى كود مشترك يمكن كتابته في مكان واحد ثم استخدامه في أماكن متعددة. بهذه الطريقة، عندما تحتاج إلى إجراء تغييرات، ما عليك سوى تطبيق تلك التغييرات في مكان واحد.
إذاً، كيف يمكننا التخلص من التكرار في ملفات إعدادات Webpack الخاصة بنا؟ هناك إضافة لذلك!

إضافة webpack-merge: دمج الإعدادات المشتركة
يمكننا استخدام إضافة webpack-merge لإدارة الكود المشترك الذي تعتمد عليه ملفات الإعدادات المتعددة. للقيام بذلك، سنقوم أولاً بتثبيت الحزمة:
yarn add --dev webpack-merge
الآن سنقوم بإنشاء ملف إعدادات Webpack ثالث يسمى webpack.config.common.js. هذا هو المكان الذي سنحتفظ فيه بالكود المشترك الخاص بنا. حالياً، تتشارك ملفات إعدادات التطوير والإنتاج الخاصة بنا نفس نقطة الدخول (entry point)، والإخراج (output)، والإضافات (plugins). كل ما يختلف بين الملفين هو الوضع (mode)، وخريطة المصدر (source map)، وخادم التطوير (dev server).
لذا، سيكون محتوى ملف webpack.config.common.js الخاص بنا كالتالي:
const path = require ( 'path' )
const { CleanWebpackPlugin } = require ( 'clean-webpack-plugin' )
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
module .exports = {
entry : './src/index.js' ,
output : {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
},
plugins : [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename : 'index.html' ,
inject : true ,
template : path.resolve(__dirname, 'src' , 'index.html' ),
}),
]
}
والآن، يمكننا دمج كائن الإعدادات المشتركة هذا في إعدادات التطوير الخاصة بنا هكذا:
const merge = require ( 'webpack-merge' )
const commonConfig = require ( './webpack.config.common' )
module .exports = merge(commonConfig, {
mode : 'development' ,
devtool : 'inline-source-map' ,
devServer : {
contentBase : './dist' ,
},
})
ويمكننا دمج كائن الإعدادات المشتركة في إعدادات الإنتاج الخاصة بنا هكذا:
const merge = require ( 'webpack-merge' )
const commonConfig = require ( './webpack.config.common' )
module .exports = merge(commonConfig, {
mode : 'production' ,
devtool : 'source-map' ,
})
انظروا كم أصبحت هذه الملفات أقصر وأنظف! رائع!

تنسيق تطبيقنا (Styling Our App)
تبدو إعدادات Webpack الخاصة بنا جيدة حتى الآن. لدينا خادم تطوير يعمل، وقد قسمنا الكود الخاص بنا إلى ملفات إعدادات للتطوير والإنتاج وملفات مشتركة. دعنا نبدأ العمل على كود تطبيقنا الفعلي الآن. الصفحة البيضاء والسوداء البسيطة مملة بعض الشيء. دعنا نضفي عليها بعض الأناقة!
في دليل src الخاص بنا، دعنا ننشئ ملف index.css ونضع الأسطر التالية من CSS بداخله:
body {
background : deeppink;
color : white;
}
ثم، في ملف ./src/index.js الخاص بنا، دعنا نستورد ملف CSS هذا:
import './index.css'
الآن، قم بتشغيل yarn start لتشغيل خادم التطوير الخاص بنا مرة أخرى. يا إلهي! نحصل على خطأ!
ERROR in ./src/index.css 1 : 5
Module parse failed: Unexpected token ( 1 : 5 )
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https: //webpack.js.org/concepts#loaders
> body {
| background: deeppink;
| color: white;
@ ./src/index.js 1 : 0 -20
ما هي هذه “المُحمّلات” (loaders) التي يتحدث عنها؟

مُحمّلات Webpack (Loaders): فهم أنواع الملفات المختلفة
في وقت سابق، ناقشنا إضافات Webpack (plugins) التي تسمح لك بتوسيع عملية بناء Webpack. يوجد أيضاً نظام بيئي من “مُحمّلات” (loaders) Webpack، والتي تساعد Webpack على فهم وتحميل أنواع الملفات المختلفة. بشكل افتراضي، يفهم Webpack كيفية التعامل مع ملفات JavaScript الخاصة بنا، لكنه لا يعرف ماذا يفعل بملفات CSS بعد. دعنا نصلح ذلك.

مُحمّلا style-loader و css-loader
يوجد مُحمّلان محددان سيكونان مفيدين لنا هنا: style-loader و css-loader. دعنا نضمّنهما في مشروعنا ثم نناقش كيفية عملهما. للبدء، وكما هو الحال دائماً، سنحتاج إلى تثبيت هاتين التبعيتين:
yarn add --dev style-loader css-loader
ثم يمكننا إضافتهما إلى ملف webpack.config.common.js الخاص بنا في قسم قواعد الوحدة النمطية (module rules section) في الأسفل:
const path = require ( 'path' )
const { CleanWebpackPlugin } = require ( 'clean-webpack-plugin' )
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
module .exports = {
entry : './src/index.js' ,
output : {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
},
plugins : [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename : 'index.html' ,
inject : true ,
template : path.resolve(__dirname, 'src' , 'index.html' ),
}),
],
module : {
rules : [
{
test : /\.css$/ ,
use: [ 'style-loader' , 'css-loader' ]
}
]
}
}
يُنشئ هذا القسم قواعد لـ Webpack حتى يعرف ما يجب فعله مع كل ملف يصادفه. خاصية test هي تعبير نمطي (regular expression) يتحقق Webpack منه مقابل اسم الملف. في هذه الحالة، نريد التعامل مع الملفات ذات الامتداد .css. ثم، تخبر خاصية use أداة Webpack بالمُحمّل أو المُحمّلات التي يجب استخدامها للتعامل مع الملفات المطابقة للمعايير.
ملاحظة هامة: الترتيب هنا يهم! تُقرأ مُحمّلات Webpack من اليمين إلى اليسار. لذا، سيتم تطبيق css-loader أولاً، ثم سيتم تطبيق style-loader.
الآن، ماذا تفعل هذه المُحمّلات فعلياً لنا؟
css-loader: يفسر ويحل ملفاتCSSالمستوردة التي تشير إليها فيJavaScriptالخاص بك. لذا في هذه الحالة، يساعدcss-loaderفي جعل هذا السطر يعمل:import './index.css'style-loader: يحقنCSSفي نموذج كائن المستند (DOM). بشكل افتراضي، يأخذstyle-loaderملفاتCSSالتي يصادفها ويضيفها إلىDOMداخل علامةstyle.
دعنا نعيد تشغيل خادم التطوير الخاص بنا عن طريق إنهاء العملية الحالية (إذا كانت لا تزال قيد التشغيل) ثم بدء تشغيلها مرة أخرى باستخدام yarn start. الآن، في متصفح الويب، يجب أن ترى هذا على https://localhost:8080/:

يا له من ألوان زاهية!
ملاحظة حول مُحمّلات Webpack الأخرى
لن نغطي مُحمّلات لأنواع الملفات الأخرى في هذا المقال، ولكن كن على دراية بأن هناك مُحمّلاً لكل شيء يمكن تخيله!
- يمكنك استخدام
file-loaderأوurl-loaderلتحميل الصور والأصول الأخرى. - يمكنك استخدام
sass-loaderللتعامل مع تحويل ملفاتSass/SCSSإلىCSSقبل توجيه هذا الإخراج إلىcss-loaderوstyle-loader. - يمكن لـ
Webpackالتعامل مع ملفاتLessأيضاً باستخدامless-loaderإذا كان هذا هو تفضيلك.
الخلاصة هي: لأي نوع ملف معين، يوجد مُحمّل يمكنه التعامل معه.
مُحمّل Babel-loader: دعم أحدث ميزات JavaScript
حسناً، بالعودة إلى تطبيقنا التجريبي. لقد كتبنا بضعة أسطر فقط من JavaScript حتى الآن. سيكون من الجيد لو تمكنا من كتابة JavaScript باستخدام ميزات جديدة لا تحظى بدعم جيد في كل المتصفحات بعد. Babel هو مُجمّع (compiler) لـ JavaScript يمكنه تحويل كود ES6+ إلى كود ES5. وكما خمنت، هناك مُحمّل لذلك: babel-loader.
لإعداد babel-loader، سنتبع التعليمات الموجودة في دليل التثبيت الخاص بهم. أولاً، سنقوم بتثبيت تبعياتنا:
yarn add --dev babel-loader @babel/core
بعد ذلك، سنضيف قاعدة جديدة إلى مصفوفة قواعد الوحدة النمطية (module rules array) في ملف webpack.config.common.js الخاص بنا:
const path = require ( 'path' )
const { CleanWebpackPlugin } = require ( 'clean-webpack-plugin' )
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
module .exports = {
entry : './src/index.js' ,
output : {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
},
plugins : [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename : 'index.html' ,
inject : true ,
template : path.resolve(__dirname, 'src' , 'index.html' ),
}),
],
module : {
rules : [
{
test : /\.css$/ ,
use: [ 'style-loader' , 'css-loader' ]
},
{
test : /\.(js|jsx)$/ ,
exclude: /[\\/]node_modules[\\/]/ ,
use: {
loader : 'babel-loader' ,
},
},
]
}
}
سيخبر هذا Webpack أنه عندما يصادف ملفات .js أو .jsx، يجب استخدام Babel لتحويل الكود. نستخدم خاصية exclude للتأكد من أن Babel لا يحاول تحويل ملفات JavaScript في دليل node_modules الخاص بنا. هذه تبعيات طرف ثالث (third-party dependencies) كان يجب أن يتم التعامل معها بالفعل من قبل مطوريها.
بعد ذلك، سنضيف تبعية أخرى لإعداد مسبق (preset) لـ Babel:
yarn add --dev @babel/preset-env
ثم سنقوم بإنشاء ملف .babelrc حيث يمكننا إجراء إعدادات Babel أخرى حسب الحاجة. سنبقي ملفنا بسيطاً جداً ونحدد فقط إعداد Babel preset الذي نريد استخدامه:
{
"presets" : [ "@babel/preset-env" ]
}
وأخيراً، دعنا نكتب بعض كود ES6 في ملف ./src/index.js الخاص بنا:
import './index.css'
const p = document .createElement( 'p' )
p.textContent = 'Hello from webpack!'
document .body.appendChild(p)
const p2 = document .createElement( 'p' )
const numbers1 = [ 1 , 2 , 3 , 4 , 5 , 6 ]
const numbers2 = [ 7 , 8 , 9 , 10 ]
const numbers3 = [...numbers1, ...numbers2]
p2.textContent = numbers3.join( ' ' )
document .body.appendChild(p2)
هذا مثال بسيط جداً، لكننا نستخدم هنا عامل الانتشار (spread operator) لدمج مصفوفتين. الآن، إذا أوقفنا العملية الجارية وقمنا بتشغيل yarn start مرة أخرى، يجب أن نرى هذا في المتصفح:

رائع! كل شيء يعمل بشكل جيد.
مشكلة اختفاء التنسيقات المؤقت (FOUC)
إذا قمت بتعطيل ذاكرة التخزين المؤقت (cache) في متصفحك وأعدت تحميل صفحة تطبيقنا التجريبي، فقد تلاحظ وميضاً طفيفاً تظهر فيه الصفحة بـ HTML غير منسق، ثم يتحول لون خلفية الصفحة إلى الوردي ويصبح النص أبيض مع تطبيق التنسيقات.
ينتج هذا السلوك عن طريقة عمل style-loader. كما ذكرنا أعلاه، يأخذ style-loader ملفات CSS ويضعها في علامة style ضمن HTML الخاص بك. وبسبب ذلك، توجد فترة زمنية قصيرة لم يتم فيها إلحاق علامة style بعد!
هذا مقبول في بيئة التطوير، لكننا بالتأكيد لا نريد هذا النوع من السلوك أن يحدث في بيئة الإنتاج. دعنا نصلح ذلك.
إضافة MiniCssExtractPlugin: استخراج ملفات CSS منفصلة
بدلاً من حقن CSS في HTML كعلامات style، يمكننا استخدام MiniCssExtractPlugin لإنشاء ملفات CSS منفصلة لنا. سنستخدم هذا في إعدادات الإنتاج الخاصة بنا، بينما نواصل استخدام style-loader فقط في إعدادات التطوير.
أولاً، دعنا نثبت التبعية في مشروعنا:
yarn add --dev mini-css-extract-plugin
الآن في ملف webpack.config.common.js الخاص بنا، دعنا نزيل قاعدة CSS لأننا سنتعامل مع هذا بشكل مختلف في بيئتي التطوير والإنتاج. يتبقى لدينا هذا في إعداداتنا المشتركة:
const path = require ( 'path' )
const { CleanWebpackPlugin } = require ( 'clean-webpack-plugin' )
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
module .exports = {
entry : './src/index.js' ,
output : {
filename : 'main.js' ,
path : path.resolve(__dirname, 'dist' )
},
plugins : [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename : 'index.html' ,
inject : true ,
template : path.resolve(__dirname, 'src' , 'index.html' ),
}),
],
module : {
rules : [
{
test : /\.(js|jsx)$/ ,
exclude: /[\\/]node_modules[\\/]/ ,
use: {
loader : 'babel-loader' ,
},
},
]
}
}
الآن، في ملف webpack.config.dev.js الخاص بنا، دعنا نضيف مرة أخرى style-loader و css-loader اللذين أزلناهما للتو من إعداداتنا المشتركة:
const merge = require ( 'webpack-merge' )
const commonConfig = require ( './webpack.config.common' )
module .exports = merge(commonConfig, {
mode : 'development' ,
devtool : 'inline-source-map' ,
devServer : {
contentBase : './dist' ,
},
module : {
rules : [
{
test : /\.css$/ ,
use: [ 'style-loader' , 'css-loader' ]
},
]
}
})
وأخيراً، في ملف webpack.config.prod.js الخاص بنا، دعنا نضيف mini-css-extract-plugin الجديد:
const merge = require ( 'webpack-merge' )
const MiniCssExtractPlugin = require ( 'mini-css-extract-plugin' );
const commonConfig = require ( './webpack.config.common' )
module .exports = merge(commonConfig, {
mode : 'production' ,
devtool : 'source-map' ,
module : {
rules : [
{
test : /\.css$/ ,
use: [
MiniCssExtractPlugin.loader,
'css-loader' ,
],
},
],
},
plugins : [
new MiniCssExtractPlugin({
filename : '[name].[contenthash].css' ,
}),
]
})
هذا يختلف قليلاً لأنه في الواقع إضافة ومُحمّل في نفس الوقت، لذا فهو يذهب في قسم قواعد الوحدة النمطية (module rules) وفي قسم الإضافات (plugins). لاحظ أيضاً أننا نستخدم الأقواس المربعة في اسم ملفنا لتعيين name ديناميكياً إلى اسم ملف المصدر الأصلي، ونضمّن أيضاً contenthash، وهو تجزئة (hash) (سلسلة أبجدية رقمية) تمثل محتويات الملف.
الآن إذا قمت بتشغيل yarn build هذه المرة لإنشاء بناء الإنتاج، يجب أن تحصل على مخرجات في طرفيتك تبدو هكذا:

لاحظ أنه يولد ملف CSS الآن، ويتضمن content hash في اسم الملف. حسناً، تم حل المشكلة! لا مزيد من الوميض عند تحميل الصفحة في الإنتاج لأن لدينا التنسيقات متضمنة كعلامة link لملف CSS فعلي.
تجاوز ذاكرة التخزين المؤقت (Cache Busting)
بما أننا قمنا بتضمين تجزئة المحتوى (content hash) في ملف CSS الذي تم إنشاؤه، فقد حان الوقت للحديث عن تجاوز ذاكرة التخزين المؤقت (cache busting). لماذا، قد تسأل، نريد تضمين تجزئة المحتوى في أسماء ملفاتنا؟ لمساعدة المتصفح على فهم متى تغير الملف!
يحاول متصفحك أن يكون مفيداً عن طريق تخزين الملفات التي رآها من قبل مؤقتاً. على سبيل المثال، إذا زرت موقعاً إلكترونياً، واضطر متصفحك إلى تنزيل أصول مثل ملفات JavaScript أو CSS أو الصور، فقد يقوم متصفحك بتخزين هذه الملفات مؤقتاً حتى لا يضطر إلى طلبها من الخادم مرة أخرى. هذا يعني أنه إذا زرت الموقع مرة أخرى، يمكن لمتصفحك استخدام الملفات المخزنة مؤقتاً بدلاً من طلبها مرة أخرى، وبالتالي تحصل على وقت تحميل صفحة أسرع وتجربة أفضل.
إذاً، ما هي المشكلة هنا؟ تخيل لو كان لدينا ملف يسمى main.js مستخدم في تطبيقنا. ثم يزور مستخدم تطبيقك ويقوم متصفحه بتخزين ملف main.js مؤقتاً. الآن، في وقت لاحق، قمت بإصدار كود جديد لتطبيقك. تغيرت محتويات ملف main.js. ولكن، عندما يزور نفس المستخدم تطبيقك مرة أخرى، يرى المتصفح أنه يحتاج إلى ملف main.js، ويلاحظ أن لديه ملف main.js مخزناً مؤقتاً، ويستخدم فقط الإصدار المخزن مؤقتاً. المستخدم لا يحصل على الكود الجديد الخاص بك!
لحل هذه المشكلة، الممارسة الشائعة هي تضمين تجزئة المحتوى (content hash) في اسم كل ملف. كما ناقشنا سابقاً، تجزئة المحتوى هي تمثيل نصي لمحتويات الملف. إذا لم تتغير محتويات الملف، فإن تجزئة المحتوى لا تتغير. ولكن، إذا تغيرت محتويات الملف، فإن تجزئة المحتوى تتغير أيضاً. ولأن اسم الملف سيتغير الآن عندما يتغير الكود، سيقوم المتصفح بتنزيل الملف الجديد لأنه لن يكون لديه اسم الملف المحدد هذا في ذاكرة التخزين المؤقت الخاصة به.
تضمين تجزئة المحتوى في أسماء الملفات
لتضمين تجزئة المحتوى (content hash) في أسماء ملفات JavaScript الخاصة بنا، سنقوم بتعديل سطر واحد فقط من الكود في ملف webpack.config.common.js. هذا السطر:
filename: 'main.js'
سيتغير إلى هذا السطر:
filename: '[name].[contenthash].js'
بحيث يبدو الملف بأكمله هكذا:
const path = require ( 'path' )
const { CleanWebpackPlugin } = require ( 'clean-webpack-plugin' )
const HtmlWebpackPlugin = require ( 'html-webpack-plugin' )
module .exports = {
entry : './src/index.js' ,
output : {
filename : '[name].[contenthash].js' , // هذا السطر هو الفرق الوحيد
path : path.resolve(__dirname, 'dist' )
},
plugins : [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename : 'index.html' ,
inject : true ,
template : path.resolve(__dirname, 'src' , 'index.html' ),
}),
],
module : {
rules : [
{
test : /\.(js|jsx)$/ ,
exclude: /[\\/]node_modules[\\/]/ ,
use: {
loader : 'babel-loader' ,
},
},
]
}
}
الآن إذا قمت بتشغيل yarn build، سترى أن كلاً من ملفات JavaScript و CSS الخاصة بك تتضمن تجزئات المحتوى:

إذا قمت بتشغيل yarn build مرة أخرى وقارنت مخرجاتك الجديدة بمخرجاتك القديمة، ستلاحظ أن تجزئات المحتوى هي نفسها تماماً في كلتا المرتين. ولكن، إذا قمت بتعديل ملف ./src/index.js بأي شكل من الأشكال ثم قمت بتشغيل yarn build مرة أخرى، فستحصل على تجزئة محتوى جديدة لأن المحتوى قد تغير! جرب ذلك!
تصغير ملفات CSS (Minifying CSS)
أخيراً وليس آخراً، قد نرغب في تصغير ملفات CSS الخاصة بنا. نحن بالفعل نقوم بتصغير JavaScript لبناء الإنتاج، لكننا لم نقم بتصغير CSS بعد. دعنا نفعل ذلك.
يمكننا تصغير CSS باستخدام optimize-css-assets-webpack-plugin. دعنا نثبت هذه التبعية الآن:
yarn add --dev optimize-css-assets-webpack-plugin
الآن يمكننا إضافة ذلك إلى قسم التحسين (optimization section) في ملف webpack.config.prod.js الخاص بنا:
const merge = require ( 'webpack-merge' )
const MiniCssExtractPlugin = require ( 'mini-css-extract-plugin' )
const OptimizeCssAssetsPlugin = require ( 'optimize-css-assets-webpack-plugin' )
const commonConfig = require ( './webpack.config.common' )
module .exports = merge(commonConfig, {
mode : 'production' ,
devtool : 'source-map' ,
module : {
rules : [
{
test : /\.css$/ ,
use: [
MiniCssExtractPlugin.loader,
'css-loader' ,
],
},
],
},
plugins : [
new MiniCssExtractPlugin({
filename : '[name].[contenthash].css' ,
}),
],
optimization : {
minimizer : [
new OptimizeCssAssetsPlugin({
cssProcessorOptions : {
map : {
inline : false ,
annotation : true ,
},
},
}),
],
},
})
الآن إذا قمنا بتشغيل yarn build ثم فحصنا محتويات دليل dist الخاص بنا، يمكننا أن نرى أن CSS الناتج مصغّر. رائع!
body { background : #ff1493 ; color : #fff } /*# sourceMappingURL=main.66e0d6aeae6f3c6fb895.css.map */
ولكن انتظر! إذا نظرنا إلى ملف JavaScript الناتج، فإنه غير مصغّر! هممم. لقد كان مصغّراً من قبل، فماذا حدث هنا؟
المشكلة هي أننا الآن نقوم بتهيئة قسم optimization minimizer في إعدادات Webpack يدوياً. عندما لا يكون هذا القسم موجوداً في ملف إعدادات Webpack، فإن Webpack يستخدم افتراضياً تفضيلاته الخاصة للمصغّرات، والتي تتضمن تصغير JavaScript عندما يتم تعيين mode إلى production. بما أننا الآن نتجاوز هذه الإعدادات الافتراضية عن طريق إضافة تفضيلاتنا لتصغير أصول CSS، فسنحتاج أيضاً إلى تضمين تعليمات صريحة حول كيفية رغبتنا في أن يقوم Webpack بتصغير أصول JavaScript.
إضافة TerserWebpackPlugin: تصغير ملفات JavaScript
يمكننا تصغير ملفات JavaScript الخاصة بنا باستخدام TerserWebpackPlugin. دعنا نبدأ بتثبيت هذه التبعية:
yarn add --dev terser-webpack-plugin
ثم، في ملف webpack.config.prod.js الخاص بنا، دعنا نضيف terser-webpack-plugin إلى إعدادات optimization minimizer في أسفل الملف:
const merge = require ( 'webpack-merge' )
const MiniCssExtractPlugin = require ( 'mini-css-extract-plugin' )
const OptimizeCssAssetsPlugin = require ( 'optimize-css-assets-webpack-plugin' )
const TerserPlugin = require ( 'terser-webpack-plugin' )
const commonConfig = require ( './webpack.config.common' )
module .exports = merge(commonConfig, {
mode : 'production' ,
devtool : 'source-map' ,
module : {
rules : [
{
test : /\.css$/ ,
use: [
MiniCssExtractPlugin.loader,
'css-loader' ,
],
},
],
},
plugins : [
new MiniCssExtractPlugin({
filename : '[name].[contenthash].css' ,
}),
],
optimization : {
minimizer : [
new OptimizeCssAssetsPlugin({
cssProcessorOptions : {
map : {
inline : false ,
annotation : true ,
},
},
}),
new TerserPlugin({
// Use multi-process parallel running to improve the build speed
// Default number of concurrent runs: os.cpus().length - 1
parallel : true ,
// Enable file caching
cache : true ,
sourceMap : true ,
}),
],
},
})
الآن إذا قمنا بتشغيل yarn build ونظرنا إلى المخرجات في دليل dist، يجب أن نرى أن كلاً من ملفات CSS وملفات JavaScript الخاصة بنا مصغّرة. ها قد وصلنا!
خلاصة ما تعلمناه
إذا كنت قد تابعت حتى هذه النقطة، فأنا أهنئك!

دعنا نراجع ما تعلمناه حتى الآن:
Webpackهو أداة بناء (build tool) لتجميع الأصول (asset bundling) وإدارة التبعيات (dependency management).- يمكن تهيئة
Webpackبواسطة ملف إعدادات (config file). - الإضافات (
Plugins) تعدل وتوسع عملية بناءWebpack. - المُحمّلات (
Loaders) توجهWebpackحول كيفية التعامل مع أنواع الملفات المختلفة. - يمكن استخدام
clean-webpack-pluginلإزالة مخلفات البناء القديمة من دليلdist. - يساعد
html-webpack-pluginفي إدارة ملفHTML، بما في ذلك حقنJavaScriptفي الملف عبر علاماتscript. - ينشئ
webpack-dev-serverخادم تطوير لتسهيل عملية التطوير المحلي. - من المفيد أن يكون لديك إعدادات
Webpackمنفصلة للتطوير والإنتاج. - يمكنك مشاركة ودمج ملفات الإعدادات باستخدام إضافة
webpack-merge. - يمكننا التعامل مع تنسيق تطبيقنا عن طريق تضمين مُحمّلات مثل
css-loader،style-loader،sass-loader،less-loader، وmini-css-extract-plugin(الذي يعمل كإضافة ومُحمّل). - يمكننا تضمين بناء جملة (
syntax) وميزاتJavaScriptجديدة باستخدامBabelوbabel-loader. - يمكننا تضمين تجزئات المحتوى (
content hashes) في أسماء ملفاتنا للمساعدة في تجاوز ذاكرة التخزين المؤقت وإدارة الإصدارات الجديدة من الكود المنشور. - يمكننا تصغير
CSSالخاص بنا باستخدامoptimize-css-assets-webpack-plugin. - يمكننا تصغير
JavaScriptالخاص بنا باستخدامterser-webpack-plugin.
الخطوات التالية: تعمّق أكثر في Webpack
خلال هذا المقال، قمنا بإنشاء إعدادات Webpack محترمة جداً. جميع هذه التقنيات التي ناقشناها هي معايير صناعية وشائعة الاستخدام في المشاريع على مستوى الشركات الكبرى. ولكن لا يزال هناك المزيد!
تشمل مواضيع Webpack المتقدمة الأخرى:
- تقسيم الكود (
code splitting) - التحميل الكسول (
lazy loading) - إزالة الكود غير المستخدم (
tree shaking) - وغيرها الكثير!
إذا كنت مهتماً باستكشاف Webpack بمفردك، فإنني أوصي بشدة بقراءة الأدلة الرسمية لـ Webpack.
مرة أخرى، يمكن العثور على جميع الأكواد التي استعرضناها في هذا الدليل على GitHub:
شكراً للقراءة، وبرمجة سعيدة!

الخلاصة التقنية
يُقدم هذا الدليل رحلة متعمقة في بناء إعدادات Webpack 4 من الألف إلى الياء، مع التركيز على الممارسات المثلى لبيئتي التطوير والإنتاج. لقد أظهرنا كيف يمكن لأداة تجميع الأصول هذه أن تُحدث فرقاً جوهرياً في كيفية إدارة التبعيات، وتحسين أداء التطبيقات، وتبسيط سير عمل المطورين. من خلال استخدام الإضافات مثل CleanWebpackPlugin و HtmlWebpackPlugin، والمُحمّلات مثل babel-loader و css-loader، بالإضافة إلى استراتيجيات تجاوز ذاكرة التخزين المؤقت وتصغير الأكواد، يصبح بالإمكان إنشاء تطبيقات ويب قوية وفعالة. إن تقسيم الإعدادات إلى ملفات مشتركة وخاصة ببيئة معينة، واستخدام webpack-merge، يعزز من قابلية الصيانة ويقلل من التكرار، مما يضمن مرونة عالية في المشاريع الكبيرة. هذا النهج لا يضمن فقط تجربة مستخدم سلسة، بل ويُحسّن أيضاً من كفاءة عملية البناء والتطوير بشكل عام.