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



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

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

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

نوع المُحتوى


التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

أسئلة وأجوبة

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

التصنيفات

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

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

  1. إن تزايد تعقيد تطبيقات الوِب في وقتنا الحالي جعل من ضرورة زيادة قابلية تطبيقات الوِب للتوسعة والتطوير أمرًا في غاية الأهمية. وعلى الرغم من أن الحلول القديمة المخصصة لكتابة شيفرات جافاسكربت و jQuery كانت فعَالة وكافية إلى حدٍ ما، ولكن بناء تطبيق وِبٍ في وقتنا الحاضر يتطلب درجة كبير من الانضباط والمنهجية الرسمية في تطوير البرمجيات. وهذه بعض الأمثلة على الممارسات الجيدة في تطوير البرمجيات: استخدام اختبارات الوحدة (Unit tests) وذلك للتأكد من أن تعديلات ما على الشيفرة البرمجية لن يعطّل شيفرة أُخرى. استخدام عملية كشف الأخطاء المحتملة (Linting) لضمان كتابة شيفرة برمجية خالية من الأخطاء. استخدام طرق مختلفة للبنية الهيكلية للشيفرة البرمجية لكلّ من وضع التطوير ووضع النشر. بالإضافة إلى ذلك فإن معايير الوِب بشكلها الحالي طرحت تحديات جديدة لتطوير تطبيقات الوِب. على سبيل المثال صفحات الوِب الحالية تُنشئ الكثير من الطلبات غير المتزامنة والّذي بدوره يؤدي إلى خفض أداء تطبيق الوِب انخفاضًا كبيرًا بسبب معالجة طلبات ملفات التنسيق والجافاسكربت. إذ أن لكل طلبٍ من هذه الطلبات (حتى وإن كان لملف صغير الحجم) فإنه سيحتاج إلى ترويسة اتصال (Header) وكُلفة إنشاء اتصال (handshakes). وهذه المشكلة تحديدًا تُعالجُ من خلال تجميع الملفات معًا لتصبح في ملف واحد وبذلك سنستدعي ملف جافاسكربت وملف تنسيق بدلًا من المئات الملفات. من الشائع في وقتنا الحالي استخدام لغات ذات معالجة مُسبّقة (language preprocessors) مثل: SASS و JSX والّتي تُترجم من هذه اللغات إلى ملفات جافاسكربت وملفات تنسيقات. بالإضافة إلى ذلك شاع أيضًا استخدام المحوّل (Transpilers) وهو محوّل يغيّر الشيفرة البرمجية من إصدار معين للغة جافاسكربت إلى إصدار أقدم، وذلك لضمان عمل هذه الشيفرة البرمجية على جميع المتصفحات. مثل المحوّل Babel. إن هذه المهام (والّتي لا علاقة لها بالمنطق البرمجي لتطبيق الوِب بحد ذاته) تمثل عبئًا على على المطور. ولكن لحسن الحظ يوجد حلٌ لهذا الأمر وهو منفذ المهام (Task Runner) والّذي جاء لمساعدتنا في أتمتة هذه المهام حتى نستطيع أن نحسّن من بيئة التطوير مع التركيز على بناء المنطق البرمجي لتطبيق الوِب. بمجرد ضبط الإعدادات الخاصة بمنفذ المهام كلّ ما عليك فعله هو استدعاء أمرٍ على الطرفية وستُنفذ بعدها المهام المؤتمتة. سأستخدم الأداة Gulp كمنفذ للمهام وذلك لأنه مناسب للمبرمجين وسهل التعلّم ومفهوم (تحدثنا في مقال سابق كيفية استخدام هذه الأداة بكلّ التفاصيل). مقدمة سريعة لأداة Gulp تتألف واجهة برمجة التطبيقات (API) لهذه الأداة من أربع دوالّ: gulp.src gulp.dest gulp.task gulp.watch في المثال التالي نلاحظ أن المهمة my-first-task ستستخدم ثلاثة دوال من أصل الأربعة. gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) }); عند تنفيذ المهمة my-first-task ستصغّر أولًا جميع الملفات المطابقة للنمط ‎/public/js/**/*.js‏ ومن ثمّ ستُنقل إلى المجلد build. الجميل في التعليمة ‏pipe()‎. هي أنه يمكنك أخذ مجموعة من ملفات الإدخال وتمريرها عبر الأنبوب لتنفيذ بعض التحويلات المناسبة عليهم ومن ثمّ إعادة ملفات الخرج المطلوبة. لجعل الأمور أكثر توافقية. غالبًا ما تنفذ العمليات المطلوبة في الإنبوب (مثل: minify()‎) من خلال مكتبات مدير الحزم npm، ونتيجة ذلك من النادر جدًا في التطبيق الفعلي لهذه االعمليات أن نحتاج لكتابة تفاصيل هذه العمليات كتابةً يدوية إلا إذا أردت أن تُعيد تسمية الملفات في الأنبوب. من الجدير بالذكر أن Gulp يَستخدم المجاري (streams) الّتي توفرها node.js، وهذا يسمح بتمرير البيانات الّتي ستُعالَج عبر الأنابيب (pipes) وهذا ما تفعله الدالة ‎.pipe()‎؛ لشرحٍ تفصيليٍ عن المجاري في node.js، سأحيلك إلى هذه المقالة. الخطوة التالية لفهم Gulp هي فهم مصفوفة تبعيات المهام. gulp.task('my-second-task', ['lint', 'bundle'], function() { ... }); في هذا المثال إن المهمة my-second-task تُعيد نتيجة الدالة المجهولة (anonymous function)، وذلك بعد اكتمال مهمة lint ومهمة التجميع bundle. وبذلك يُسمح لنا بفصل الاهتمامات عن بعضها بعضًا كما يمكنك أيضًا إنشاء سلسلة من المهام الصغيرة بمسؤولية واحدة مثل تحويل LESS إلى CSS. وإنشاء مهمة رئيسية (Master Task) والّتي ستستدعي ببساطة جميع المهام الأخرى (الصغيرة) عبر مصفوفة من تبعيات المهام. وأخيرًا لدينا التعليمة gulp.watch والّذي يراقب التغييرات في ملف (أو ملفات) ما والمطابق لنمط مرّر لها، وما إن يحدث تغيير ما في هذا الملف حتى تُنفذّ سلسلة من المهام المحددة. gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) }) في المثال السابق أي تغيير في الملفات الّتي تطابق النمط /public/js/**/*.js سيؤدي إلى تشغيل مهمة كشف الأخطاء المحتملة lint وبعدها مهمة إعادة التحميل reload. إن الاستخدام الشائع للتعليمة gulp.watch هو تشغيل عمليات إعادة التحميل المباشر في المتصفح، وهي ميزة رائعة جدًا أثناء مرحلة التطوير ولن تستطيع العمل بدونها بمجرد أن تجربها. وإلى هنا نستطيع القول بأننا فهمنا كلّ ما سنحتاج لاستخدامه في الأداة Gulp. أين سنستخدم أداة Webpack؟ عندما نستخدام نمط CommonJS فإن تجميع كلّ ملفات الجافاسكربت ليصبحوا في ملف واحد ليس بهذه البساطة. إذ إن الخاصية (entry point) والّتي تُسند عادةً للقيمة index.js أو app.js مع سلسلة من التعليمات require أو import الموجودة في أعلى الملف. وسيكون شكل ملف جافاسكربت في الإصدار ES5 على الشكل التالي: var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2'); وسيكون شكل ملف جافاسكربت في الإصدار ES6 على الشكل التالي: import Component1 from './components/Component1'; import Component2 from './components/Component2'; إن المثالين السابقين يجلبان التبعيات قبل تنفيذ بقية الشيفرات البرمجية في ملف app.js، وممكن أن يكون لهذه التبعيات تبعيات أخرى والّتي ستُجلب أيضًا. وبالإضافة إلى ذلك من الممكن أن تُستدعى نفس التبعية في أماكن متعددة في تطبيق الوِب خاصتك. ولكننا نريد جلب هذه التبعية مرة واحدة فقط. لهذا فأن كانت شجرة التبعيات بعمق عدة مستويات (أي التبعيات ذات هرمية كبيرة) فعندها ستزداد صعوبة تجميع هذه التبعيات في ملف واحد. ولكن لحسن الحظ يوجد حلّ رائع لهذه المشكلة وهو مُجمّع الحزم (الوحدات) مثل:Browserify أو Webpack. لماذا يفضل المطورون استخدام Webpack بدلًا من Gulp؟ بما أن أداة Webpack (تحدثنا في مقال سابق عن كيفية استخدام مجمع الحزم Webpack وأشهر طرق استعمالها في المشاريع) لتجمّيع الحزم وأداة Gulp لتنفيذ المهام من الممكن أن نتوقع أن نرى استخدام هاتين الأداتين مع بعضهما بعضًا، ولكن هنالك توجّه عام نحو استخدام Webpack بدلًا من Gulp وخصيصًا في مجتمع مطوري React. ولكن لما هذا التوجه؟ ببساطة إن قوة أداة Webpack مكنتها من تنفيذ الغالبية العظمى من المهام الملقاة على عاتق منفذ المهام (مثل: Gulp أو أي منفذ مهام عمومًا). فعلى سبيل المثال توفر Webpack خيارات تصغير وخرائط الشيفرة البرمجية المحوّلة Source Maps (لمزيد من المعلومات عنها يمكنك الإطلاع على المقال التالي) للشيفرة البرمجية المُجمعّة. بالإضافة إلى ذلك يمكن استخدامها كوسيط (Middleware) من خلال خادم مخصص يدعى webpack-dev-server والّذي يدعم كلًا من إعادة التحميل المباشر (live reloading) وإعادة التحميل النشط (hot reloading) لصفحات الوِب (والّتي سنتحدث عنها لاحقًا في هذا المقال). وكما يمكنك أيضًا تحويل الشيفرة البرمجية (transpiling) من إصدار جافاسكربت حديث مثل ES6 إلى إصدارٍ قديم مثل: ES5. ويمكنك أيضًا استخدام طريقة المعالجات المُسبقة (pre-processors) أو المُلحقة (post-processors) لملفات التنسيق. وبذلك تُترك عملية إختبار الوحدة (Unit Tests) وعملية كشف الأخطاء المحتملة (linting) كمهام رئيسية مستقلة نظرًا من كوننا قلصنا ما لا يقلّ عن ستة مهام محتملة للأداة Gulp لمهمتين فقط. يلجأ العديد من المطورين لاستخدام NPM Scripts بدلًا من استخدام Gulp، وذلك لتجنب إضافة أداة Gulp إلى المشروع في حين وجود بديل قوي ينوب عنها. في الحقيقة إن السيئة الرئيسية لاستخدام Webpack هي صعوبة ضبط إعداداته، مما يجعله خيارًا غير مرغوب به في حال أردنا إنشاء مشروع وتشغيله بأسرع وقتٍ ممكن. طرق إعداد منفذ مهام سنتعرف على ثلاث طرق لإنشاء وإعداد منفذ مهام مساعد في المشاريع البرمجية. وكلّ واحدٍ منهم سينفذ المهام التالية: إعداد خادم تطوير مع ميزة إعادة التحميل المباشر (live reloading) فور حدوث أي تعديل على صفحة الوِب المُراقبة. تجميع ملفات التنسيق والجافاسكربت (بالإضافة إلى تحويل الشيفرة البرمجية للغة جافاسكربت من الإصدار ES6 إلى ES5، وتحويل ملفات التنسيق من SASS إلى ملفات CSS وخرائط الشيفرة البرمجية المحوّلة) وذلك بطريقة قابلة للتطوير والتوسّع لهذه الملفات المحوّلة. تشغيل اختبار الوحدة (Unit Tests) سواءً كمهمة قائمة بحد ذاتها أو في وضع المراقبة. تشغيل عملية الكشف عن الأخطاء المحتملة (Linting) سواءً كمهمة قائمة بحد ذاتها أو في وضع المراقبة. توفير القدرة على جمع كلّ المهام السابقة عبر أمر واحد لكتابته عبر الطرفية. وجود أمر آخر لتجميع الملفات وضغطها أو تنفيذ تحسينات أخرى عليها من أجل عملية النشر. وستكون طرق إعداد منفذ المهام على الشكل التالي: Gulp + Browserify Gulp + Webpack Webpack + NPM Scripts سنعتمد في تطبيقنا العملي على استخدام React للواجهات الأمامية. في الحقيقة أردت في البداية أن أعمل بدون إطار عمل، ولكن استخدام React سيُبسط مسؤوليات منفذ المهام، إذ سيلزمنا فعليًا وجود ملف HTML واحد فقط، بالإضافة إلى أن React يعمل بسلاسة مع نمط CommonJS ولذلك اعتمدت أخيرًا على استخدامه. سنعمد على توضيح جميع المزايا والعيوب الخاصة بكل طريقة إعداد، وذلك لمنحك المعلومات الكاملة والصورة الشاملة لاتخاذ قرارٍ صائبٍ يناسب احتياجات المشروع الّذي تعكف على تطويره. سننشئ مشروعًا على Git وسنضيف له ثلاثة تفريعات من أجل اختبار كلّ طريقة من الطرق git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup) لنناقش الآن كلّ تفريعة (طريقة) من هذه الفروع على حدة. ستكون البنية الهرمية لمجلد المشروع على الشكل التالي: - app - components - fonts - styles - index.html - index.js - index.test.js - routes.js سنتطرق لشرح أهم الملفات الموجودة في المشروع وذلك حرصًا على كمال المعلومة. سيكون الملف index.html على الشكل التالي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="bundle.css"> <title>Gulp Browserify Setup</title> </head> <body> <div id="app"></div> <script src="bundle.js"></script> </body> </html> نلاحظ أن هذا الملف بسيط جدًا إذ إنه يُحمّلُ تطبيق React في الوسم <div id="app"></div> ولن نستخدم إلا ملف واحد للتنسيقات وملف آخر للجافاسكربت، في الواقع في طريقة الإعداد هذه لن نستخدم حتى ملف التنسيق bundle.css. وسيكون الملف index.js على الشكل التالي: import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app')); نلاحظ أن هذا الملف سيكون بمثابة نقطة الدخول (entry point) لتطبيقنا. إذ إننا بصدد تحميل React Router في الوسم div مع السِمة app الّتي أشرنا لها في الملف السابق. وسيكون الملف routes.js على الشكل التالي: import React from 'react'; import {Route, IndexRoute} from 'react-router'; import App from './components/App'; import HomePage from './components/home/HomePage'; import AboutPage from './components/about/AboutPage'; import ContactPage from './components/contact/ContactPage'; export default ( <Route path="/" component={App}> <IndexRoute component={HomePage} /> <Route path="about" component={AboutPage} /> <Route path="contact" component={ContactPage} /> </Route> ); نلاحظ أن هذا الملف يحدد مسارات المشروع. وستكون المسارات / و ‎/about و ‎/contact مرتبطة مع المكونات HomePage و AboutPage و ContactPage على التتالي. وسيكون الملف index.test.js على الشكل التالي: import expect from 'expect'; describe('Array', () => { it('should return -1 when the value is not present', () => { expect(-1).toBe([1,2,3].indexOf(4)); }); it('should correctly filter elements in an array', () => { const arr = [1,2,3,4,5]; const newArr = arr.filter(el => el > 3); expect(newArr.length).toBe(2); }); it('should correctly map elements in an array', () => { const arr = [1,2,3,4,5]; const newArr = arr.map(el => el * 2); expect(newArr).toEqual([2,4,6,8,10]); }); }); describe('Object', () => { it('should convert an int to a string with the toString method', () => { const val = 5; const valToString = val.toString(); expect(val).toNotBe(valToString); expect(valToString).toBe('5'); }); it('should correctly test whether an object has a certain property', () => { const obj = {a: 1, b: 2}; expect(obj.hasOwnProperty('a')).toBe(true); expect(obj.hasOwnProperty('c')).toBe(false); }); }); describe('String', () => { it('should convert a string to lowercase', () => { const str = 'HELLO'; const strLower = str.toLowerCase(); expect(strLower).toBe('hello'); }); it('should convert a string to uppercase', () => { const str = 'hello'; const strLower = str.toUpperCase(); expect(strLower).toBe('HELLO'); }); it('should remove spaces from both ends of a string', () => { const str = ' hello there '; const strTrimmed = str.trim(); expect(strTrimmed).toBe('hello there'); }); it('should split the string into an array of strings based on the provided delimiter', () => { const str = 'she-sells-seashells-down-by-the-seashore'; const strSplit = str.split('-'); expect(Array.isArray(strSplit)).toBe(true); expect(strSplit.length).toBe(7); }); }); نلاحظ أن هذا الملف يحتوي على سلسلة من اختبارات الوحدة (Unit Tests) والّتي ستختبر سلوك الشيفرة الأصلية للجافاسكربت (Native JavaScript) في عملية النشر الفعلية للتطبيق يمكنك إنشاء اختبار لكل مكون React (اختبار واحد على الأقل لكل مكوّن يعالج حالة معينة) واختبار السلوك الخاص بإطار العمل React. وعلى أية حال يكفي أن يكون لديك اختبار وحدة بسيط والّذي نستطيع تشغيله في وضع المراقبة (watch mode عند تفعيل هذا الوضع تثبت ملفات مخصصة لمراقبة أي تغييرات تحصل في الملفات الأخرى وفي حال حصول أي تغييرات في الملفات سيعاد ترجمة الملفات للحصول على الخرج الجديد). وسيكون الملف components/App.js على الشكل التالي: import React, {PropTypes} from 'react'; import Header from './common/Header'; class App extends React.Component { render() { return ( <div> <Header /> {this.props.children} </div> ); } } App.propTypes = { children: PropTypes.object.isRequired }; export default App; يمكن اعتبار الملف السابق حاوية لجميع مكونات العرض الموجودة لدينا. إذ تحتوي كلّ صفحة لدينا على الترويسة <Header/> بالإضافة إلى this.props.children والّذي يقيّم لعرض محتواه في الصفحة نفسها. فعلى سبيل المثال إذا كان في المتصفح /contact سيقيّم إلى ContactPage. وسيكون الملف components/home/HomePage.js على الشكل التالي: import React from 'react'; import {Jumbotron, Grid, Row, Col, Panel} from 'react-bootstrap'; class HomePage extends React.Component { render() { return ( <div className="HomePage"> <Jumbotron> <Grid> <h1>Home Page</h1> </Grid> </Jumbotron> <Grid> <Row> <Col sm={6}> <Panel header="Panel 1"> <p>Content A</p> <p>Content B</p> </Panel> </Col> <Col sm={6}> <Panel header="Panel 2"> <p>Content A</p> <p>Content B</p> </Panel> </Col> </Row> </Grid> </div> ); } } export default HomePage; هذا الملف سيكون الصفحة الرئيسية المعروضة. استخدمت react-bootstrap نظرًا لأن نظام الشبكة (Grid) في إطار العمل Bootstrap ممتاز لإنشاء صفحات متجاوبة. وعند استخدامه استخدامًا صحيحًا سيُقّلل عدد استعلامات الوسائط (media queries) الّتي سنكتبها للأجهزة صغيرة الحجم تقليلًا كبيرًا. المكونات الأخرى المتبقية (مثل: Header و AboutPage و ContactPage) مبنية بطريقة مشابهة (باستخدام react-bootstrap بدون التلاعب بالحالة [state manipulation]). أما الآن لنتحدث أكثر عن ملفات التنسيق. منهجية الملفات التنسيق أسلوبي المفضل في تنسيق ملفات React هو امتلاك ملف تنسيق خاص لكلّ مكوّن من المكونات. إذ سيكون هذا التنسيق ضمن نطاق هذا المكون فقط. ستلاحظ أنه في كلّ مكون من المكونات هنالك وسم div ذو مستوى أعلى وسيكون أسم الصنف التابع له مطابق لأسم المكون نفسه. لذلك المكون HomePage.js سيكون مغلف بوسم له الشكل التالي: <div className="HomePage"> ... </div> وسيكون هنالك أيضًا ملف HomePage.scss والّذي سيكون مهيكل على الشكل التالي: @import '../../styles/variables'; .HomePage { // Content here } لماذا هذه المنهجية مفيدة للغاية؟ لأنها تنتج لنا وحدات (Modular) ذات جودة عالية. مما يُلغي الكثير من المشاكل غير المرغوب بها في التنسيق. لنفترض أن لدينا مكونين React وهما Component1 و Component2. وفي كلّ واحد منهما نريد إعادة تنسيق الوسم h2 ليصبح حجم الخط كما في الملف التالي: /* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } } إن حجم الخط المخصص للوسم h2 في المكونين Component1 و Component2 مكتوبان بمعزل عن بعضهما البعض سواءً أكان المكونين متجاورين أو متداخلين لن يؤثر أحدهما على الآخر. أي أن المكون سيبدو شكله مثل ما خُطط له تمامًا بغض النظر عن مكان وجوده. في الحقيقة إن الأمر ليس بهذه السهولة دائمًا، ولكنه وبكل تأكيد خطوة كبيرة باتجاه أفضل الممارسات الصحيحة للتنسيق. وبالنسبة للتنسيقات المُسبقة للمكونات (per-component styles) أحب أن أخصص مجلد تنسيقات styles يحتوي على جميع التنسيقات العامة في الملف global.scss بجانب تنسيقات SASS مخصصة والّتي تكون مسؤولة عن معالجة التنسيقات المخصّصة (في حالتنا مثل: ‎_fonts.scss و ‎_variables.scss من أجل الخطوط والمتغيّرات على التوالي). تتيح لنا هذه التنسيقات العامة تحديد الشكل العام للتطبيق بأكمله بينما يمكن للتنسيقات الأخرى المخصصة أن تُستورد بحسب الحاجة لها. والآن بعد أن استكشفنا بعمق الشيفرة البرمجية المشتركة بين كلِّ طرق إعداد منفذ المهام. لننتقل إلى أول طريقة منهم. 1. طريقة Gulp + Browserify يتكون ملف gulpfile.js من 22 سطرًا من تعليمات لاستيراد المكتبات والحزم و150 سطرًا من الشيفرات البرمجية الأخرى. لذا ومن أجل الإيجاز سنراجع بالتفاصيل أهم النقاط الرئيسية في هذا الملف (مثل: js و css و server و watch و default). محزم ملفات جافاسكربت // ضبط إعداد Browserify const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); } نلاحظ في الدالّة bundle أن ملفات الجافاسكربت ستُحزّم باستخدام Browserify وستُستخدم خرائط الشيفرة البرمجية المحوّلة (Source maps) في وضع التطوير بينما ستُستخدم عملية التصغير لملفات جافاسكربت في وضع الإنتاج. في الحقيقة إن هذا النهج سيء وذلك لعدة أسباب إحداها أن المهمة ستقسّم إلى ثلاثة أجزاء منفصلة. إذ في البداية سننشئ كائن للتحزيم وليكن b وسنمرر له بعض الخيارات المطلوبة، ومن ثمّ نحدد بعض مُعالِجات الأحداث (event handlers). ثم لدينا مهمة Gulp والّتي يجب أن نمرر لها اسم الدالة بدلًا من تمريرها سطريًا (إن b.on('update') تستخدم نفس الطريقة). إن هذه الطريقة غير أنيقة مثل الطريقة الموجودة في مهام Gulp والّتي تتطلب فقط تمرير gulp.src وتوجيه بعض التغييرات. هناك مشكلة أخرى تجبرنا على اتباع طرق مختلفة لإعادة تحميل ملفات html و css و js في المتصفح. لاحظ طريقة إعداد مهمة المراقب الموضحة في الشيفرة التالية: gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); }); عندما سيحصل أي تغيير في ملف html سيعاد تشغيل مهمة html من جديد. gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); }); إن آخر تعليمة في الأنبوب هي عملية إعادة التحميل المباشرlivereload() إذا كانت قيمة الخاصية NODE_ENV ليست production، ستُحدث الصفحة في المتصفح تحديثًا تلقائيًا. يتكرر نفس السيناريو من أجل مُراقب ملفات التنسيق. إذ عند حدوث أي تغيير في أحد ملفات التنسيق تُستدعى المهمة css ويعاد تشغيلها من جديد، وأخر تعليمة في الأنبوب تكون لاستدعاء التحميل المباشر في المتصفح وذلك لعرض التغييرات مباشرة. غير أن المراقب الخاص بملفات الجافاسكربت لا يستدعي مهمة js على الإطلاق، وإنما معالج الأحداث في الأداة Browserify يتعامل مع إعادة التحميل باستخدام منهجية مختلفة تمامًا (يطلق عليها اسم مبدل الوحدات النشط [hot module replacement]). إن التضارب في هذه المنهجية مزعج للغاية. ولكنه للأسف ضروري وإذا استدعينا تعليمة إعادة التحميل المباشر في نهاية دالة bundle فعندها سيؤدي ذلك إلى إعادة بناء كلّ ملفات جافاسكربت في حال حدث أي تغيير في أي ملف منهم. من الواضح أن هذه المنهجية غير قابلة للتوسّع والتطوّر. إذ كلما زاد عدد ملفات جافاسكربت كلما استغرقت عملية إعادة التجميع وقتًا أطول. وفجأة عملية البناء الّتي كانت تستغرق 500 ميلي ثانية ستستغرق 30 ثانية مما سيُعيق منهجية التطوير الرشيق (agile development). محزم ملفات التنسيق gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); }); إن المشكلة الأولى الّتي تظهر هنا هي تضمين المكتبات أو ملفات التنسيق. إذ يجب علينا أن نتذكر أن نغيّر اسم الملف في كلّ مرة يظهر بها إصدار جديد سواءً للمكتبة (أو لأي ملف تنسيق) وذلك بحذف اسم المكتبة القديمة واستبدالها بالجديدة إلى مصفوفة gulp.src؛ بدلًا من إضافة أمر التضمين في مكان مناسب أكثر في الشيفرة البرمجية. المشكلة الرئيسية الأخرى هي المنطق المعقد في كلّ أنبوب. إذ إنني اضطررت لإضافة مكتبة من مستودع مدير الحزم NPM تدعى gulp-cond فقط لإضافة الجمل الشرطية في تعليمات الأنبوب، والنتيجة النهائية لتعليمات الأنبوب ليس من السهل قراءتها (لأن الأقواس الثلاثية في كلّ مكان!). مهمة الخادم gulp.task('server', () => { nodemon({ script: 'server.js' }); }); هذه المهمة واضحة للغاية. إذ إنه في الأساس مُغلّف لأمر استدعاء nodemon server.js والّذي سيُشغّل ملف server.js في بيئة Node.js. استخدامنا nodemon بدلًا من node إذ ستؤدي أي تغييرات تحدث في الملف إلى إعادة تشغيل الخادم. افتراضيًا nodemon تُعيد تشغيل العملية عند أي تغيير في ملف الجافاسكربت ولهذا السبب من المهم لتضمين ملف nodemon.json للحدّ من مجاله. { "watch": "server.js" } لنراجع الآن الشيفرة البرمجية الخاصة بملف server.js const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express(); يؤدي هذا الإعداد إلى تعيين الدليل الأساسي للخادم والمنفذ (port) بناءً على بيئة Node.js. وينشئ نسخة من express. app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir))); ستضيف هذه التعليمات برمجية وسيطة (Middleware) وهي connect-livereload وذلك لأنها الضرورية لإعداد ميزة إعادة التحميل المباشر، كما ستضيف هذه التعليمات أيضًا برمجية وسيطة ساكنة (Static Middleware) والّتي ستتعامل مع الملحقات الثابتة. app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); }); إن الشيفرة البرمجية السابقة هي مجرد مسار بسيط لواجهة برمجة التطبيقات. فإذا انتقلت إلى المسار localhost:3000/api/sample-route في المتصفح سترى ما يلي: { website: "Toptal", blogPost: true } في الواجهات الخلفية للتطبيق سيكون لدينا مجلد كامل مخصص لمسارات الواجهة البرمجية للتطبيقات، وملفات منفصلة لإنشاء اتصالات مع قاعدة البيانات. هكذا أُضيف المسار وبهذه البساطة لإثبات أننا يمكننا بناء الواجهات الخلفية بعد بناء الواجهات الأمامية. app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); }); هذا المسار الشامل يعني أنه مهما يكن الرابط التشعبي الّذي تودّ الذهاب إليه من خلال المتصفح فسيعرض لك الخادم صفحة index.html الوحيدة، وبعدها تأتي مهمة موجّه المسارات في React لعرض الصفحة المناسبة الّتي طلبتها من جانب العميل بدلًا من جانب الخادم. app.listen(port, () => { open(`http://localhost:${port}`); }); الشيفرة البرمجية السابقة تُخبر نسخة express للتنصت على المنفذ الّذي خصصناه وفتح تبويب جديد في المتصفح لعنوان الرابط الّذي طلبتَ الوصول إليه. الشيء الوحيد الّذي لا يعجبني في طريقة إعداد الخادم هو: app.use(require('connect-livereload')({port: 35729})); نظرًا لأننا نستخدم بالفعل gulp-livereload في ملفنا gulpfile، مما يدل على وجود مكانين منفصلين وجب علينا استخدام إعادة التحميل المباشر. إعداد المهام الافتراضية gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); }); تنفذ المهمة السابقة من خلال كتابة الأمر gulp في الطرفية. الغريب في الأمر أنك تحتاج لاستخدام runSequence من أجل تنفيذ التعليمات تسلسليًا. عادة ما تنفذ المهام بالتوازي ولكن هذه الطريقة ليست مطلوبة دومًا. فعلى سبيل المثال نحتاج لتشغيل مهمة clean قبل مهمة html للتأكد من أن مجلدات الهدف فارغة قبل نقل الملفات إليها. عندما صدرت النسخة الرابعة من gulp 4 كان إحدى مميزاتها أنها دعمت تعليمتي gulp.series لتنفيذ التعليمات بالتسلسل و gulp.parallel لتنفيذ التعليمات بالتوازي. وفضلًا عن ذلك، إن عملية إنشاء واستضافة التطبيق من خلال أمرٍ واحد هو فعلًا أمرٌ رائع، وإمكانية فهم أي جزء من أجزاء سير العمل بسهولة مثل سهولة اختبار مهمة ما في تسلسل التنفيذ. بالإضافة إلى ذلك يمكننا تفكيك المهام الكبير إلى أجزاء أصغر لنهج أكثر دقة في إنشاء واستضافة التطبيق. على سبيل المثال يمكننا إعداد مهمتين منفصلتين لإجراء عمليات إختبار المهام الأخرى وكشف الأخطاء المحتملة (linting). أو يمكن أن يكون لدينا مهمة المضيف والّتي ستُشغل الخادم والمراقب. هذه القدرة على تنظيم المهام قوية جدًا، وخاصةً عندما يتغيّر تطبيقك ويتطلب المزيد من المهام المؤتمتة. بناء التطبيق وتخصيصه لوضع التطوير مقابل وضع النشر if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production'; من خلال استخدام مكتبة yargs الموجودة في مستودعات مدير الحزم NPM. يمكننا تزويد الأداة Gulp بالرايات (Flags). هنا مثلًا سنوجّه ملف gulpfile لتعيين بيئة Node.js في وضع النشر إذا مرّرنا الراية ‎--prod مع التعليمة في الطرفية. سيستخدم هذا المتغيّر PROD كشرط للتمييز بين وضع التطوير ووضع النشر في ملف gulpfile. على سبيل المثال أحد الخيارات الّتي نمررها لإعدادات الأداة Browserify هي: plugin: PROD ? [] : [hmr, watchify] إن التعليمة الشرطية في الشيفرة السابقة مفيدة جدًا لأنها توفر علينا كتابة ملف gulpfile منفصل لكل من وضع التطوير ووضع النشر، والّذي سيحتوي على الكثير من التعليمات المكررة. بدلًا من ذلك يمكننا تمرير gulp --prod لتشغيل وضعية النشر في تنفيذ المهام. أو تمرير gulp html --prod لتشغيل المهمة html فقط في وضع النشر. من جهة أخرى رأينا سابقًا كيف أن تمرير التعليمات الشرطية عبر الأنبوب مثل: .pipe(cond(!PROD, livereload())) ليست من السهل قراءتها. ولكن في نهاية المطاف أنها مسألة تفضيلات شخصية إذا ما كنت تريد استخدام المتغيير المنطقي (PROD) أو إنشاء ملفين منفصلين (gulpfile) للإعداد. لننتقل الآن إلى طريقة الإعداد الثانية، وهي عندما نبدل الأداة Browserify لتحل محلها الأداة Webpack. 2. طريقة Gulp + Webpack نلاحظ أن ملف الإعداد gulpfile انخفض حجمه انخفاضًا كبيرًا إذ يحتوي الآن على 12 سطرًا من تعليمات استيراد المكتبات و 99 سطرًا من الشيفرات البرمجية الأخرى. إذا فحصنا من المهمة الافتراضية: gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); }); يتطلب إعداد تطبيق الوب خاصتنا الآن خمسة مهام فقط، بدلًا من تسعة. وهو في الحقيقة تحسنٌ كبير. بالإضافة إلى ذلك ألغينا الحاجة لاستخدام ميزة إعادة التحميل المباشر livereload، كما أن مهمة المراقب أصبحت كالتالي: gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); }); هذا يعني أن المراقب لن يشغل سلوك إعادة بناء التطبيق من جديد. وكميزة إضافية لن نحتاج إلى نقل الملف index.html من app إلى dist أو build بعد الآن. وبالعودة إلى فكرة تقليل المهام، نلاحظ إن المهام المخصصة لكلٍ من ملفات html و css و js و fonts استبدلت بمهمة واحدة: gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); }); بكلّ بساطة شغّل مهمتي clean و html بالتسلسل، وبمجرد ما ينتهي تنفيذهُم إجلب نقطة الدخول الخاصة بتطبيق الوب خاصتنا ومرّره إلى الأنبوب من خلال الأداة Webpack ثم مررها إلى ملف الإعداد الخاص بالأداة Webpack وهو webpack.config.js وذلك لتهيئه وأرسال الحزمة المُجمّعة الناتجة إلى baseDir (إما dist أو build اعتمادًا على الملف الإعداد ل Node.js). يمكنك إلقاء نظرة على ملف الإعداد webpack.config.js الخاص بالأداة Webpack، لكننا لن نشرحه كله، وإنّما سنشرح الخصائص المهمة المسندة للكائن module.exports. devtool: PROD ? 'source-map' : 'eval-source-map', تعيّن هذه التعليمة نوع خرائط الشيفرة البرمجية المحوّلة (source maps) والّتي سيستخدمها Webpack. تدعم Webpack مجموعة من الخياراتٍ المتنوعة من خرائط الشيفرة البرمجية المحوّلة. ويوفر كلّ وخيارٍ منهم توازنًا مختلفًا في الأداء. فمنهم خرائط ذات تفاصيل كثيرة مقابل خرائط ذات تفاصيل قليلة ولكن إعادة بنائها سريع (وهو الوقت المستغرق لإعادة تجميع الملف بعد إجراء التغييرات). وبناءً عليه يمكننا استخدام خرائط ذات تفاصيل قليلة من أجل زيادة سرعة إعادة التحميل في وضع التطوير واستخدام خرائط ذات تفاصيل كثيرة في وضع النشر. entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ] هذه هي نقطة الدخول إلى مُجمّع الحزم. لاح المصفوفة المُمررة لنقطة الدخول، وهذا يدلّ على إمكانية استخدام نقاط دخول متعددة. في حالتنا نقطة الدخول هي الملف app/index.js، وكذلك نقطة الدخول الخاصة لإعداد إعادة التحميل النشط للوحدات (hot module reloading) output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, وهنا نحدد مجلد الخرج والّذي سيوضع فيه الملفات المترجمة والمجمّعة. إن أكثر خيارٍ مربكٍ هنا هو publicPath والّذي يعيّن عنوان الرابط التشعبي الّذي سيُجمّع فيه ويوضع على الخادم. لذلك على سبيل المثال إذا كان publicPath هو /public/assets عندها ستكون الحزمة المجمّعة في هذا المسار /public/assets/bundle.js على الخادم. devServer: { contentBase: PROD ? './build' : './app' } هذه التعليمة ستُعلِم الخادم بالمجلد الجذر الّذي ستستخدمه في مشروعك. إذا شعرت بالإرتباك في طريقة تعيين مجلد الخرج لوضع الملف المجمّع فما عليك سوى تذكر هذه الدلالات لكلّ مما يلي: path + filename: الموقع المحدد للحزمة المُجمّعة في الشيفرة البرمجية للمشروع. contentBase (مثل: ملف الجذر /) + publicPath: موقع الحزمة على الخادم. plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], هذه بعض الملحقات الوظيفية الّتي ستعزز وظائف Webpack بطريقة ما. فعلى سبيل المثال إن الملحق webpack.optimize.UglifyJsPlugin هو المسؤول عن تصغير ملفات جافاسكربت. loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ] هذه هي المُحمّلات والّتي تعالج بشكل أساسي الملفات الّتي تُضمّن بواسطة require()، وهي تشبه إلى حدٍ ما الأنابيب في Gulp والّتي تمكنك من ربط المُحمّلات مع بعضهم بعضًا. لنستكشف أحد المُحمّلات: {test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'} إن الخاصية test تُخبر الأداة Webpack أن الملفات الّتي يجب على المُحمّل التعامل معها يجب أن تحقق التعبير النمطي المُمرر لهذه الخاصية. في حالتنا تكون /\.scss$/. أما الخاصية loader فهي تُحدد المُحمّل الّذي يجب أن نستخدمه. هنا نحدد المُحملات style و css و resolve-url و sass والّتي ستُنفذ بترتيب عكسي. يجب علي أن أعترف بأنني لست ماهرًا في بناء جملة تحديد المُحملات loader3!loader2!loader1. ولكن متى يجب علينا قراءة شيفرة برمجية من اليمين إلى اليسار؟ عدا هذا السطر. على الرغم من ذلك تعدّ المُحمّلات ميزة قوية جدًا لمُجمّع الحزم Webpack. في الحقيقة يسمح لنا المُحمّل الّذي استخدمته للتو باستيراد ملفات SASS بداخل الشيفرة البرمجية للجافاسكربت. فعلى سبيل المثال يمكننا استيراد ملفات التنسيق المخصّصة العامة الّتي عملنا عليها في المشروع في ملف الّذي حددنا كنقطة دخول وهو index.js وسيكون شكله كما يلي: import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app')); وبطريقة مشابهة في مكوّن الترويسة يمكننا استيراد الملف import './Header.scss' المرتبط بهذا المكون، وهذا وبكل تأكيد ينطبق على كافة المكونات الأخرى في التطبيق. من وجهة نظري الشخصية أرى بأن هذا الأمر يعدُّ تغييرًا ثوريًا في عالم التطوير بلغة جافاسكربت. إذ لا داعي للقلق بشأن تجميع ملفات التنسيق وتصغيرها وحتى خرائط الشيفرة البرمجية المحوّلة (source maps) أيضًا، وذلك لأن مُحمّلنا يتعامل مع كلّ هذه الأمور عوضًا عنا. حتى إعادة التحميل النشط للوحدات (hot module reloading) ستعمل على ملفات التنسيق خاصتنا، ومن ثمّ فإن القدرة على التعامل مع تعليمات الاستيراد من داخل ملف الجافاسكربت سيجعل من عملية التطوير أبسط من الناحية المفاهيمية، وأكثر تناسقًا، وتقلل من تبديل السياق في التطوير (وذلك عند استخدام أدوات مساعدة مختلفة)، والمنطق البرمجي أسهل في التفكير. لإعطاء ملخص موجز حول كيفية عمل هذه الميّزة مع مُجمّع الحزم Webpack وكيف سيضع ملفات التنسيق في ملف جافاسكربت المُجمّع. في الحقيقة لا يقتصر الأمر على ملفات التنسيق وحسب بل يمكنه أيضًا فعل ذلك من أجل الصور والخطوط. {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} إن الشيفرة البرمجية السابقة توجّه المُحمّل إلى تضمين الصور والخطوط بداخل الملف المُجمّع إن كان حجمهم لا يتعدى 100 كيلوبايت، وإلا فسيعرضهم كملفات منفصلة، كما يمكننا في أي وقت تغيير حجم المسموح به، ليصبح مثلًا 10 كيلوبايت. هذا هو باختصار طريقة إعداد مُجمّع الحزم Webpack وفي الحقيقة إنه يتطلب قدرًا لا بأس به من الإعداد. ولكن فوائد استخدامه رائعة للغاية. على الرغم من أن مُجمّع الحزم Browserify لديه أيضًا ملحقاته الجميلة وتحويلاته ولكنه لا يستطيع أن ينافس محملات Webpack من ناحية الخصائص الوظيفية المُضافة. 3. طريقة Webpack + NPM Scripts في هذه الطريقة من الإعداد سنستخدم npm scripts مباشرة بدلًا من الاعتماد على Gulp لأتمتة مهامنا. سيكون ملف package.json على الشكل التالي: "scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" } لتشغيل طريقة البناء من أجل وضع التطوير أو وضع النشر يمكنك كتابة الأوامر npm start و npm run start:prod على التتالي. نلاحظ أن هذا الملف أفضل وبكل تأكيد من ملف الإعداد gulpfile الّذي بنيناه سابقًا في هذا المقال. إذ اختصرنا الكثير من الشيفرات البرمجية لتكون عدد التعليمات بدلًا من 150 أو 99 تعليمة أصبحت 19 تعليمة (12 تعليمة إذا استثنينا التعليمات الخاصة بوضع النشر لأن معظمها مشابهة للتعليمات في وضع التطوير الّتي في بيئة Node.js لتصبح في وضع النشر). إن العيب الوحيد هو أن التعليمات مبهمة إلى حدٍ ما بالموازنة مع التعليمات الخاصة للأداة Gulp وليست معبرة مثلها. فعلى سبيل المثال لا توجد (على حسب اطلاعي على الأقل) بوجود تعليمة واحدة تشغّل npm script لينفذ تعليمات معينة تنفيذًا متسلسلًا وتعليمات أخرى ليُنفذها تنفيذًا متوازيًا. وإنما تعليمة واحدة لكل منهما. ومع ذلك هنالك الكثير من الميّزات الكبيرة لهذا المنهج. بالإضافة إلى أنه يوجد حزمة مقابلة لكل مكتبة من المكتبات الّتي كنا نستخدمها في السابق مع أداة Gulp. فبدلًا من استخدام المكتبات الموجودة في Gulp مثل: gulp-eslint gulp-mocha gulp-nodemon etc يمكننا استخدام الحزم التالية في Webpack: eslint mocha nodemon etc نقلًا عن كوري هاوس في مقالته "لماذا تركت أداة Gulp و Grunt وتوجهت إلى NPM Scripts": يحدد كوري هاوس ثلاث مشاكل رئيسية وهي: مشكلة الاعتماد على منشئي الملحقات. مشكلة تنقيح الأخطاء المرهقة. مشكلة التوثيقات الرسمية المفككة والضعيفة. ومن وجهة نظري أتفق مع جميع هذه المشاكل. 1. مشكلة الاعتماد على منشئي الملحقات مثلًا عندما تُحدّث مكتبة مثل: eslint، ستحتاج المكتبة المرتبطة بها gulp-eslint إلى تحديث مناظر لكي يتوافقوا مع بعضهم بعضًا. فإذا فقد المشرف الاهتمام بالعمل على تعديل وصيانة المكتبة، فإن الإصدار المخصص لأداة gulp من هذه المكتبة لن يتزامن مع النسخة الأصلية للمكتبة. وينطبق نفس الشيء عند إنشاء مكتبة جديدة. فعلى سبيل المثال إذا قام شخص ما بإنشاء مكتبة "xyz" وأطلقها، واحتجت فجأة إلى نسخة مخصصة من المكتبة للعمل مع أداة Gulp مثل: gulp-xyz. بمعنى آخر، هذا النهج غير قابل للتوسّع. ومن الناحية المثالية، نريد نهجًا مثل Gulp ولكن بإمكانه استخدام المكتبات الأصلية مباشرة. 2. مشكلة تنقيح الأخطاء المرهقة على الرغم من أن المكتبات مثل gulp-plumber تساعد في تخفيف هذه المشكلة تخفيفًا كبيرًا، إلا أنه من المعروف أن الإبلاغ عن الأخطاء في gulp ليس مفيدًا جدًا. فإذا كان هناك تعليمة واحدة خاطئة في الأنبوب فإنها سترمي استثناءً غير قابل للمُعالجة، وعندما تلاحق المشكلة في المكدس ستظهر مشكلة تبدو غير مرتبطة تمامًا بالسبب الحقيقي للمشكلة الّتي ظهرت في الشيفرة البرمجية. لهذا السبب يمكن أن يجعل من علمية تنقيح الأخطاء كابوسًا في بعض الحالات. ومهما بحثت عن حلول لهذه المشاكل سواءً على محركات البحث مثل: Google أو موقع Stack Overflow فلن يساعدك هذا البحث فعليًا إذا كان الخطأ مبهمً أو مضللًا. 3. مشكلة الوثائق الرسمية المفككة والضعيفة في كثير من الأحيان ألاحظ أن أحد مكتبات gulp الصغيرة يكون توثيقها الرسمي محدودًا للغاية. أظن بأن هذا الأمر يرجعُ من أن المُنشئ عادة لهذه المكتبة يكون هدفه الأساسي استخداماته الخاصة. بالإضافة إلى ذلك من الشائع أن تنّظر إلى التوثيق الرسمي لكلّ من الملحقات الإضافية للأداة Gulp والمكتبة الأصلية (الّتي تعتمد عليها تلك الملحقات)، مما يعني الكثير من تبديل السياق وزيادة كمية القراءة إلى الضعف بسبب ذلك. الخاتمة يبدو لي أنه من الواضح جدًا أن Webpack أفضل من Browserify وأن NPM scripts أفضل من Gulp، على الرغم من أن كلّ خيارٍ منهم له فوائده وعيوبه. ومن المؤكد أيضًا أن تعابير وتعليمات Gulp أكثر مقروئية وملاءمة للاستخدام من NPM scripts، ولكنك ستدفع الثمن غاليًا في جميع عمليات التعقيد المضافة. قد لا تكون كلّ طريقة من طرق الإعداد السابقة مثالية لتطبيقك، ولكن إذا رغبت في تجنب عدد هائل من تبعيات التطوير وتجربة تنقيح الأخطاء المحبطة، فإن Webpack مع NPM scripts هي الطريقة المناسبة لك. آمل أن تجد هذه المقالة مفيدة في اختيار الأدوات المناسبة لمشروعك القادم. ترجمة -وبتصرف- للمقال Webpack or Browserify & Gulp: Which Is Better? لصاحبه Eric Grosse
  2. تتوفر عدِّة «أدوات بناء» (build tools) تعمل على تسهيل عملية تطوير مواقع الويب وأتمتة المهام الروتينية مثل «تصغير» (minify) ملفات JavaScript و HTML و CSS والصور، وتحويل ملفات LESS و SASS إلى CSS، والتحقق مباشرةً من صحة البنية اللغوية لملفات JavaScript والكثير من الأمور التي ستغيّر من طريقة عملك وتجعلها أكثر سرعةً وكفاءة. سنشرح في درسنا هذا إحدى أدوات البناء المشهورة وهي الأداة Gulp بالإضافة إلى أساسيات استخدامها، شرحًا مدعمَّا بالأمثلة العملية. ملاحظة مهمة: هذا الدليل كتب من أجل الإصدار 3 من Gulp وهنالك بعض الاختلافات والتغييرات الجوهرية في الإصدار 4. لذلك، إن كنت تستعمل الإصدار 4، فهذا الدليل ليس لك لأنه هنالك بعض الدوال والواجهات البرمجية التي لن تعمل مع ذلك الإصدار. ما هي أداة البناة Gulp؟ أدوات البناء هو أمر حديث العهد ولم ينتشر إلا منذ بضعة سنوات. كانت ملفات ‎.sh التي تكتب بلغة Bash أحد أشهر أدوات البناء غير المباشرة وغير المخصصة المستعملة آنذاك لكتابة التعليمات وأتمتة بعض المهام الروتينية المحدودة مثل مراقبة الملفات وتحديثها. هذه الملفات تعمل مع الصدفة shell التي تأتي مدمجة مع أنظمة التشغيل. انتشرت أدوات بناء أخرى مثل Ant و Gradle ولكنهما كانتا ضمن عالم جافا وتحتاجان إلى استعمال لغة XML لضبطهما وهي اللغة التي لا يحبها مطوري الويب عدا عن تعلم لغة جديدة لا يألفونها. اللغة التي يحبها مطورو الويب هي جافاسكربت وهنا جاءت أداتا البناء Gulp و Grunt لسد هذه الفجوة. تتفوق أداة البناء Gulp على منافستها Grunt بأنها أسرع بكثير لأنها تنفذ المهام على التوازي (على عكس Grunt) كما أنها تمرر الملفات المفتوحة عبر مجاري Streams داخلية. أضف إلى ذلك أن اللغة المستعملة في ضبط مهام Gulp هي جافاسكريبت بالتأكيد وهي اللغة التي يحبها مطورو الويب كثيرًا. المتطلبات المسبقة سيجري العمل على نظام التشغيل أوبنتو 18.04 ويجب أن تكون قد ثبَّت Node.js و npm مسبقًا. إن لم تكن Node.js مثبتةً لديك، فابدأ بتثبيتها هي ومدير الحزم npm عبر الأمر التالي: sudo apt update sudo apt install nodejs للتحقق من عملية التثبيت (وللتأكد من الإصدار المثبت مسبقًا لديك)، نفذ الأمر التالي: nodejs --version يجب أن ترى ناتجًا شبيهًا بالناتج التالي: v8.10.0 ولتكون قادرًا على تنزيل وتثبيت حزم npm، تحتاج إلى تنزيل npm (مدير حزم Node.js) إن لم يكن مثبَّتًا لديك: sudo apt install npm تحقق من عملية التثبيت (أو الإصدار المثبت مسبقًا) عبر الأمر التالي: npm --version يجب أن ترى ناتجًا شبيهًا بالناتج التالي: 3.5.2 أنت الآن جاهز لإكمال هذا الدليل! تثبيت Gulp يمكن تثبيت Gulp عبر مدير الحزم npm الذي تحدثنا عنه منذ قليل، بشكل سهلٌ للغاية فكل ما عليك فعله هو تنفيذ الأمر الآتي في سطر الأوامر لتثبيت Gulp 3 لعموم النظام: npm install gulp@3.9.0 ثم عليك تهيئة مجلد المشروع الذي ستعمل عليه. نفِّذ الأمر الآتي في مجلد المشروع: npm init عليك الآن تثبيت Gulp في المشروع وإضافته إلى الاعتماديات (أي devDependencies): npm install --save-dev gulp أصبح Gulp مثبتًا في مشروعك وجاهزًا للاستخدام. البنية الهيكلية للمشروع الذي سنعمل عليه مرونة Gulp تسمح لنا باستخدامه بغض النظر عن بنية المجلدات التابعة للمشروع، كل ما يلزمك هو فهمٌ صحيحٌ كاملٌ لآلية عمله مما يسمح لك بتعديله لتلبية احتياجاتك. سنعتمد في مقالتنا البنية الآتية للمجلدات: |- app/ |- css/ |- fonts/ |- images/ |- index.html |- js/ |- scss/ |- dist/ |- gulpfile.js |- node_modules/ |- package.json هذه بنيةٌ بسيطة نستعمل فيها مجلد app لاحتواء ملفات التطوير، بينما المجلد dist (اختصار للكلمة «distribution») سيحتوي على الملفات الإنتاجية. كتابة أول مهمة من مهام Gulp عندما نُشغِّل Gulp من سطر الأوامر، فسيبحث عن وجود الملف gulpfile.js في مجلد المشروع. سيُخبِر هذا الملف Gulp ما هي الإضافات التي يجب تحميلها وما هي المهام التي يجب القيام بها. علينا إذًا إنشاء الملف gulpfile.js وتضمين gulp في بدايته كما يلي: var gulp = require('gulp'); عبارة require السابقة ستخبر Node أن تبحث في مجلد node_modules عن حزمة باسم gulp، وعند العثور عليها فستُسنَد محتوياتها إلى المتغير gulp. يمكننا الآن البدء بكتابة «مهمّة Gulp» (أي Gulp task) بالاستفادة من المتغير gulp. البنية الأساسية لمهمّة Gulp هي: gulp.task('task-name', function() { // سنضع الشيفرات هنا }); task-name تشير إلى اسم المهمّة، والتي ستُستعمَل في أي مكانٍ تريد تشغيل هذه المهمّة فيه. نستطيع تشغيل هذه المهمة بتمرير اسمها كوسيط للأمر gulp في سطر الأوامر (أي تنفيذ gulp task-name). لنجرِّب ما سبق بإنشائنا للمهمّة hello في ملف gulpfile.js السابق (الذي يحتوي على عبارة require) والتي ستطبع الجملة Hello World: gulp.task('hello', function() { console.log('Hello World'); }); يمكننا تشغيل هذه المهمة بتنفيذ الأمر gulp hello في سطر الأوامر، لا تنسَ الانتقال إلى مجلد المشروع (عبر الأمر cd في لينكس أو ماك، أو dir في ويندوز) قبل تنفيذ الأمر. مخرجات الأمر السابق هي: user@linuxbox:~/gulp-test$ gulp hello [18:34:41] Using gulpfile ~/gulp-test/gulpfile.js [18:34:41] Starting 'hello'... Hello World [18:34:41] Finished 'hello' after 127 μs حسنًا، لا تكون مهام Gulp بهذه البساطة، حيث تحتوي عادةً على دالتين تابعتين للكائن gulp بالإضافة إلى استخدام إضافة (plugin) أو أكثر من إضافات Gulp. هذا مثالٌ عن البنية العامة لمهمّات Gulp: gulp.task('task-name', function () { //الحصول على الملفات التي يجب إجراء عمليات عليها // gulp.src باستخدام الدالة return gulp.src('source-files') .pipe(aGulpPlugin()) // ومن ثم تمريرها إلى إضافة .pipe(gulp.dest('destination')) // وإخراج الملف إلى المجلد الهدف }) كما هو واضح، تتطلب المهام المفيدة في gulp دالتين هما gulp.src و gulp.dest. الدالة gulp.src تخبر Gulp ما هي الملفات التي يجب تطبيق المهمة عليها، بينما الدالة gulp.dest تخبر Gulp أين يجب إخراج الملفات بعد تنفيذ المهمّة. يَستخدم Gulp المجاري (streams) التي توفرها node.js، وهذا يسمح بتمرير البيانات التي ستُعالَج عبر الأنابيب (pipes) وهذا ما تفعله الدالة ‎.pipe()‎؛ لشرحٍ تفصيليٍ عن المجاري في node.js، فأحيلك إلى هذه المقالة. سنشرح كيفية الاستفادة من Gulp عبر استعماله لتحويل ملفات Sass إلى ملفات CSS. تحويل ملفات Sass إلى CSS عبر Gulp نستطيع استخدام إضافة باسم gulp-sass لتحويل ملفات Sass إلى CSS. عليك تثبيت الإضافة gulp-sass في مشروعك باستخدام الأمر npm install كما فعلنا سابقًا مع gulp. يفضل أيضًا استخدام الخيار ‎--save-dev لكي تُضاف الحزمة gulp-sass إلى devDependencies في ملف package.json وهذا يسهل نسخ الملف إلى مشروع آخر وتثبيت نفس الاعتماديات (عبر الأمر npm install فقط): npm install gulp-sass –save-dev علينا الآن تضمين الحزمة gulp-sass عبر require من مجلد node_modules كما فعلنا أول مرة مع gulp كي نتمكن من استخدام الإضافة: var gulp = require('gulp'); // gulp-sass تضمين إضافة var sass = require('gulp-sass'); يمكننا استخدام gulp-sass بوضع الدالة sass()‎ بدلًا من aGulpPlugin()‎ في المثال السابق. ولمّا كان الغرض من المهمّة هو تحويل ملفات Sass إلى CSS فلنسمِّها sass: gulp.task('sass', function(){ return gulp.src('source-files') .pipe(sass()) // gulp-sass استخدام إضافة .pipe(gulp.dest('destination')) }); علينا توفير ملفات مصدرية بصيغة sass -ومجلد لإخراج الناتج فيه- إلى المهمّة sass، لنبدأ بإنشاء الملف styles.scss في مجلد app/scss، ومن ثم سنضع مسار هذا الملف في الدالة gulp.src في مهمّة sass. ونريد أيضًا إخراج ملف styles.css النهائي إلى مجلد app/css الذي سيكون هو مجلد الوجهة (destination) لدالة gulp.dest. gulp.task('sass', function(){ return gulp.src('app/scss/styles.scss') .pipe(sass()) .pipe(gulp.dest('app/css')) }); سنختبر المهمّة sass الآن للتأكد من عملها عملًا صحيحًا، لكن قبل ذلك علينا استخدام دالة من دوال Sass ضمن ملف styles.scss. // styles.scss .testing { width: percentage(5/7); } إذا نفَّذتَ الأمر gulp sass في سطر الأوامر، فيجب أن يُنشَأ الملف styles.css في مجلد app/css، مع تبديل الدالة percentage(5/7)‎ وتحويلها إلى 71.42857%‎. /* styles.css */ .testing { width: 71.42857%; } تحققنا أنَّ المهمّة sass تعمل بشكلٍ سليم. أحيانًا نحتاج إلى بناء أكثر من ملف ‎.scss وتحويلها إلى ملفات CSS في نفس الوقت، وسنحتاج حينها إلى استخدام «محارف التحديد» (Globs). محارف التحديد في Node تسمح لك محارف التحديد بتمرير أكثر من ملف إلى الدالة gulp.src، وهي شبيهة بالتعابير النمطية (regular expressions) لكنها تُستعمَل خصيصًا لمسارات الملفات. عند استخدامك لمحرف تحديد (glob) فسيتحقق جهازك من أسماء الملفات والمسارات المُحدَّدة بالنمط المستخدم، فإن تمت مطابقة النمط فسيُضاف الملف إلى الدالة gulp.src. أغلبية الأنماط التي نستعملها مع Gulp تنضوي تحت لواء الأنماط الآتية: النمط ‎*.scss: رمز النجمة * هو محرفٌ خاصٌ يُطابِق أيّة ملف في المجلد المعيّن. وفي مثالنا ستُطابَق جميع الملفات التي تنتهي باللاحقة ‎.scss في المجلد الرئيسي للمشروع. النمط ‎**/*.scss: هذه حالةٌ أعم من المثال السابق، حيث ستتم مطابقة أيّة ملفات تنتهي باللاحقة ‎.scss في المجلد الرئيسي للمشروع وفي جميع المجلدات الفرعية الموجودة فيه. النمط ‎!not-me.scss: الرمز ! يعني أنَّ على Gulp استثناء هذا النمط من الملفات المُحدَّدة، وهذا مفيدٌ إن شئتَ أن تستثني ملفًا من التحويل؛ وفي مثالنا سنستثني الملف not-me.scss. النمط ‎*.+(scss|sass): إشارة الزائد + والأقواس ()‎ ستسمح لأداة Gulp بمطابقة عدِّة أنماط معًا، والتي سيُفصَل بينها بمحرف الخط العمودي |. وفي مثالنا سيُطابِق Gulp جميع الملفات التي تنتهي باللاحقة ‎.scss أو ‎.sass في المجلد الرئيسي للمشروع. بعد تعلمنا لمحارف التحديد، أصبح بإمكاننا وضع التعبير app/scss/**/*.scss بدلًا من app/scss/styles.scss، وبهذا ستُطابَق جميع الملفات التي لها اللاحقة ‎.scss في المجلد app/scss أو أيّ مجلدٍ فرعيٍ موجودٍ داخله. gulp.task('sass', function() { // scss الحصول على جميع الملفات التي تنتهي باللاحقة // app/scss والموجودة في المجلد return gulp.src('app/scss/**/*.scss') .pipe(sass()) .pipe(gulp.dest('app/css')) }) سيُضمَّن أيّ ملف Sass موجود في مجلد app/scss تلقائيًا في مهمّة sass المُعدَّلة. فلو أضفتَ ملف print.scss إلى المشروع، فستلاحظ توليد الملف print.css في مجلد app/css تلقائيًا. حسنًا، تمكّنا من تحويل جميع ملفات Sass إلى ملفات CSS بأمرٍ وحيد، لكن السؤال الآن هو: ما الفائدة من المهمّة التي أنشأناها إن كنا سنحتاج إلى تطبيق الأمر gulp sass يدويًا في كل مرة نرغب فيها بتحويل ملفات Sass إلى CSS؟ لحسن الحظ، يمكننا أن نخبر Gulp أن يُشغِّل المهمّة sass تلقائيًا في كل مرة يُحفَظ فيها أحد تلك الملفات، وهذا ما ندعوه «المراقبة» (watching). مراقبة التغييرات في ملفات Sass يوفِّر لنا Gulp الدالة watch لمعرفة إن حُفِظَ أحد الملفات. الشكل العام لدالة watch هو: gulp.watch('files-to-watch', ['tasks', 'to', 'run']); إذا أردنا مراقبة جميع ملفات Sass وتشغيل المهمّة sass عندما يحفظ أحد تلك الملفات، فعلينا أن نضع app/scss/**/*.scss بدلًا من files-to-watch ونضع ['sass'] بدلًا من ['tasks', 'to', 'run']: gulp.watch('app/scss/**/*.scss', ['sass']); وفي أغلبية الأوقات سنحتاج إلى مراقبة أكثر من نوع من الملفات في آنٍ واحد، ويمكننا إنشاء مجموعة من عمليات المراقبة مع بعضها ضمن مهمّة باسم watch: gulp.task('watch', function(){ gulp.watch('app/scss/**/*.scss', ['sass']); // عمليات المراقبة الأخرى }) إذا جرّبتَ تنفيذ الأمر gulp watch الآن، فسترى أنَّ Gulp قد بدأ مراقبة الملفات فوريًا. user@linuxbox:~/gulp-test$ gulp watch [21:10:00] Using gulpfile ~/gulp-test/gulpfile.js [21:10:00] Starting 'watch'... [21:10:00] Finished 'watch' after 12 ms وأنَّ Gulp سيُنفِّذ المهمّة sass عندما تحفظ أحد ملفات ‎.scss، جرِّب الآن فتح الملف styles.scss السابق وإضافة أيّ شيء إليه وحفظه، ثم انظر إلى ناتج Gulp: abd@linuxbox:~/gulp-test$ gulp watch [21:10:00] Using gulpfile ~/gulp-test/gulpfile.js [21:10:00] Starting 'watch'... [21:10:00] Finished 'watch' after 12 ms [21:12:03] Starting 'sass'... [21:12:03] Finished 'sass' after 69 ms ما رأيك أن نخطو خطوةً إلى الأمام ونجعل Gulp يُعيد تحميل الصفحة عندما نحفظ أحد ملفات ‎.scss لكي يظهر تأثير تغيير الصفحة مباشرةً على المتصفح، وذلك بالاستعانة بأداة Browser Sync. تحديث الصفحة آنيًا باستخدام Browser Sync يُسهِّل Browser Sync من تطوير الويب بإنشاء خادوم ويب الذي يساعدنا على تحديث الصفحات آنيًا عند تغييرها. وله ميزاتٌ أخرى، مثل مزامنة الأحداث بين أكثر من جهاز. لنبدأ أولًا بتثبيت الأداة Browser Sync كالمعتاد: npm install browser-sync –save-dev علينا بعدئذٍ أن نُضمِّن Browser Sync عبر التعليمة require: var browserSync = require('browser-sync').create(); سنحتاج إلى إنشاء مهمّة باسم browserSync للسماح لأداة Gulp بتشغيل خادوم ويب باستخدام Browser Sync، ولأننا سنُنشِئ خادوم ويب، فعلينا أن نُحدِّد للخادوم ما هو المجلد الرئيسي للموقع، والذي هو في حالتنا المجلد app: gulp.task('browserSync', function() { browserSync.init({ server: { baseDir: 'app' }, }) }) علينا أيضًا تغيير المهمّة sass قليلًا لكي يتمكن Browser Sync من تحديث أنماط CSS في المتصفح عندما تُشغَّل المهمّة sass: gulp.task('sass', function() { return gulp.src('app/scss/**/*.scss') .pipe(sass()) .pipe(gulp.dest('app/css')) .pipe(browserSync.reload({ stream: true })) }); انتهينا الآن من ضبط الأداة Browser Sync، لكن علينا تشغيل المهمّتَين watch و browserSync في نفس الوقت لكي تُحدَّث الصفحة آنيًا. من غير المقبول فتح نافذَتي سطر أوامر وتشغيل gulp browserSync في إحداها و gulp watch في الأخرى، لذا لنجعل Gulp يشغِّلهما معًا بإخبار المهمّة watch أنَّه يجب إكمال المهمّة browserSync قبل السماح بتنفيذ watch. يمكننا فعل ذلك بتمرير وسيطٍ ثانٍ إلى المهمّة watch. الشكل العام هو: gulp.task('watch', ['array', 'of', 'tasks', 'to', 'complete','before', 'watch'], function (){ // ... }) وفي حالتنا هذه سنضيف المهمّة browserSync: gulp.task('watch', ['browserSync'], function (){ gulp.watch('app/scss/**/*.scss', ['sass']); // عمليات المراقبة الأخرى }) علينا أيضًا أن نتأكد أنَّ المهمّة sass ستعمل قبل watch لكي يتم تحويل أيّة ملفات sass حُفِظَت قبل تشغيل Gulp إلى ملفات CSS. gulp.task('watch', ['browserSync', 'sass'], function (){ gulp.watch('app/scss/**/*.scss', ['sass']); // عمليات المراقبة الأخرى }); إذا شغَّلتَ gulp watch في سطر الأوامر، فسيُشغِّل Gulp المهمّتين sass و browserSync ثم بعد إكمالهما ستُشغَّل المهمّة watch. ستجد في الناتج رابط URL شبيه بالرابط الآتي http://localhost:3000 الذي إذا فتحته في نافذة المتصفح، فسترى صفحة app/index.html التي يجب أن يكون محتواها شبيهًا بما يلي: <!doctype html> <html> <head><link rel="stylesheet" href="css/styles.css"></head> <body></body> </html> شغِّل الآن المتصفح وادخل إلى الرابط السابق وستعرض أمامك صفحة فارغة، ولتجربة التحديث الآني للصفحة، فافتح ملف styles.scss وأضف لونًا للخلفية (مثلًا: body {background-color: red;} واحفظ الملف وستجد أنَّ الصفحة قد حُدِّثَت تلقائيًا! ما رأيك الآن أن نضيف قليلًا على المهمّة السابقة ونراقب تغيير ملفات HTML أو JavaScript (بالإضافة إلى ملفات ‎.sass) ومن ثم إعادة تحميل الصفحة حينها؟ يمكننا فعل ذلك بإضافة عمليتَي مراقبة، واستدعاء الدالة browserSync.reload عند حفظ الملف: gulp.task('watch', ['browserSync', 'sass'], function (){ gulp.watch('app/scss/**/*.scss', ['sass']); // تحديث الصفحة عند حفظ هذه الملفات gulp.watch('app/*.html', browserSync.reload); gulp.watch('app/js/**/*.js', browserSync.reload); }); حتى الآن قمنا بثلاثة أمور: تشغيل خادوم ويب للتطوير تحويل ملفات Sass إلى CSS إعادة تحميل الصفحة في المتصفح عند حفظ الملفات سنشرح في القسم الآتي كيفية تحسين الملفات الملحقة بصفحات الويب، وسنبدأ بتحسين ملفات CSS و JavaScript. تحسين ملفات CSS و JavaScript يُجري المطورون مهمّتَين عندما يحاولون تحسين ملفات CSS و JavaScript: تصغير الملفات وجمعها في ملفٍ واحد. إحدى المشاكل التي يواجهها المطورون عند أتمتة هذه العملية هي الصعوبة في جمع السكربتات بترتيبٍ صحيح. لنقل أننا أضفنا 3 وسوم script في صفحة index.html: <body> <script src="js/lib/a-library.js"></script> <script src="js/lib/another-library.js"></script> <script src="js/main.js"></script> </body> هذه السكربتات موجودة في مجلدين مختلفين، ومن الصعب جمعها باستخدام الإضافات التقليدية مثل gulp-concatenate؛ لكن لحسن الحظ، تأتي إضافة gulp-useref لتحل لنا هذه الإشكالية. تجمع إضافة gulp-useref أيّ عدد من ملفات CSS أو JavaScript إلى ملفٍ وحيد بالبحث عن تعليق يبدأ بالتعبير ‎<!--build:‎ وينتهي بالتعبير <‎!--endbuild--‎> الشكل العام له هو: <!-- build:<type> <path> --> ... HTML Markup, list of script / link tags. <!-- endbuild --> النوع <type> يمكن أن يكون إما js أو css أو remove. من الأفضل تحديد نوع type للملفات التي تحاول جمعها؛ وإذا ضَبطتَ type إلى remove فسيَحذف Gulp الشيفرةَ المُحدَّدةَ ولن يولِّد ملفًا. أما <path> فيشير إلى مسار الملف الذي سيولّد. إذا أردتَ أن يكون ملف JavaScript المولَّد في مجلد js باسم main.min.js فستبدو الشيفرة كالآتي: <!--build:js js/main.min.js --> <script src="js/lib/a-library.js"></script> <script src="js/lib/another-library.js"></script> <script src="js/main.js"></script> <!-- endbuild --> لنضبط الآن إضافة gulp-useref في ملف gulpfile.js. علينا أولًا تثبيت الإضافة ثم تضمينها في الملف. نفِّذ الأمر الآتي للتثبيت: npm install gulp-useref –save-dev أضف السطر الآتي إلى ملف gulpfile.js: var useref = require('gulp-useref'); تهيئة المهمّة useref شبيهة بتهيئة المهام الأخرى التي أنجزناها من قبل. هذه هي الشيفرة: gulp.task('useref', function(){ return gulp.src('app/*.html') .pipe(useref()) .pipe(gulp.dest('dist')) }); إذا شغّلتَ مهمّة useref الآن فسيأخذ Gulp ملفات JavaScript المُحدَّدة وسيجمعها في ملف dist/js/main.min.js. لكن الملف لم يُصغَّر (minified) إلى الآن، وعلينا استخدام إضافة gulp-uglify لفعل ذلك. وسنحتاج أيضًا إلى إضافة تدعى gulp-if لكي نُصغّر ملفات JavaScript دونًا عن غيرها. تثبيت الإضافة: npm install gulp-uglify --save-dev الشيفرة التي سنضيفها إلى ملف gulpfile.js: var uglify = require('gulp-uglify'); var gulpIf = require('gulp-if'); gulp.task('useref', function(){ return gulp.src('app/*.html') .pipe(useref()) // JavaScript التصغير إذا كان الملف .pipe(gulpIf('*.js', uglify())) .pipe(gulp.dest('dist')) }); يجب أن يولِّدَ Gulp الملفَ main.min.js في كل مرة تشغِّل فيها المهمّة useref. إحدى الأمور الرائعة التي لم أخبرك عنها بعد هي أنَّ إضافة gulp-useref ستحوِّل جميع السكربتات الموجودة بين ‎<!--build:‎ و <‎!--endbuild--‎> إلى ملف JavaScript وحيد الذي يُشير إلى js/main.min.js وذلك في صفحة index.html، أي أنَّ ملف index.html الناتج سيكون شبيهًا بما يلي: <!doctype html> <html> <head></head> <body> <script src="js/main.min.js"></script> </body> </html> سنستعمل نفس الطريقة لجمع ملفات CSS: <!--build:css css/styles.min.css--> <link rel="stylesheet" href="css/styles.css"> <link rel="stylesheet" href="css/another-stylesheet.css"> <!--endbuild--> يمكننا أيضًا تصغير ملف CSS الناتج، لكننا بحاجة إلى تثبيت الإضافة gulp-cssnano: npm install gulp-cssnano الشيفرة التي سنضيفها إلى ملف gulpfile.js: var cssnano = require('gulp-cssnano'); gulp.task('useref', function(){ return gulp.src('app/*.html') .pipe(useref()) .pipe(gulpIf('*.js', uglify())) // CSS التصغير إذا كان الملف .pipe(gulpIf('*.css', cssnano())) .pipe(gulp.dest('dist')) }); ستحصل الآن على ملف CSS وملف JavaScript وحيد ومُصغّر في كل مرة تُشغِّل فيها المهمّة useref. تحسين دعم المتصفحات لخاصيات CSS وشيفرة JavaSctipt نبدأ بتحسين دعم خاصيات CSS عبر مختلف المتصفحات. فمن المؤكد أنك رأيت شيفرة CSS مكتوبة بالشكل التالي: .navigation { display: -webkit-box; display: -ms-flexbox; display: flex } ولابد أنك استصعبت - أثناء كتابة شيفرة CSS - البحث عن دعم كل خاصية من خاصيات CSS لمختلف المتصفحات وتساءلت عن وجود أداة تضيف السوابق الخاصة بدعم الخاصيات في المتصفحات الأخرى مثل ‎-webkit-box و ‎-ms-flexbox. الحل بسيط جدًا وهو استعمال الإضافة gulp-autoprefixer الموجودة لهذا الغرض. نفذ الأمر التالي لتثبيت هذه الإضافة: npm install gulp-autoprefixer --save-dev أضف السطر الآتي إلى ملف gulpfile.js: const autoprefixer = require('gulp-autoprefixer'); تهيئة واستعمال هذه الإضافة - مع الإضافة gulp-sass والإضافة browser-sync التي تحدثنا عنهما في الأعلى - يكون بالشكل التالي: gulp.task("styles", function() { gulp .src("app/css/**/*.scss") .pipe(sass().on("error", sass.logError)) .pipe(autoprefixer({ // دعم آخر إصدارين للمتصفح browsers: ["last 2 versions"] }) ) .pipe(gulp.dest("css")) .pipe(browserSync.stream()); }); نحصل بذلك على شيفرة CSS السابقة عند كتابة: .navigation { display: flex } فقط لتصبح الشيفرة مدعومة على كافة المتصفحات التي لا تدعم خاصيات CSS محدَّدة مثل التي رأيناها للتو. ملاحظة: إن أردت استعمال الإضافة gulp-autoprefixer مع الإضافة gulp-cssnano (وإضافات أخرى)، فيمكنك استعمالهما سوية عبر استعمال الإضافة gulp-postcss. تمرر هذه الإضافة شيفرة CSS إلى عدة إضافات مع تحليل الشيفرة مرةً واحدةً مما يزيد من سرعة تنفيذ العملية. var postcss = require('gulp-postcss'); var gulp = require('gulp'); var autoprefixer = require('autoprefixer'); var cssnano = require('cssnano'); gulp.task('css', function () { // CSS تحديد الإضافات المراد استعمالها مع شيفرة var plugins = [ autoprefixer({browsers: ['last 2 version']}), cssnano() ]; return gulp.src('app/css/*.css') .pipe(postcss(plugins)) .pipe(gulp.dest('./dest')); }); اطلع على توثيق الإضافة الرسمي لمزيد من الأمثلة وكيفية الاستخدام. ننتقل الآن إلى تحسين دعم شيفرة JavaScript وأتحدث الآن عن دعم ميزات ES6 عبر مختلف المتصفحات. الحقيقة أن الميزات التي يوفرها الإصدار ES6 (أو ECMAScript 6) مغرية جدًا لجميع مطوري الويب ولكن نقص الدعم في مختلف المتصفحات والإصدارات القديمة هو من أكبر العقبات أمام الاستفادة من تلك الميزات. انطلاقًا من ذلك، جاءت فكرة وجود محول يدعى transpiler والذي يحول من لغة برمجية معينة إلى برمجية أخرى (في حالتنا من ECMAScript 6 إلى ES2015 أو ما قبلها). المحول الذي سنختاره هنا هو المحول الشهير Babel JS الذي يحوي الكثير من الميزات والمدعوم بقوة من قبل المجتمع. سنستعمل الإضافة gulp-babel لهذا الغرض. ثبت الإضافة عبر الأمر التالي: # Babel 6 npm install --save-dev gulp-babel # Babel 7 npm install --save-dev gulp-babel@next @babel/core أضف الشيفرة التالية إلى ملف gulpfile.js: var babel = require("gulp-babel"); gulp.task("default", function () { return gulp.src("app/js/*.js") // تحويل الشيفرة إلى الإصدار الأقدم .pipe(babel()) .pipe(gulp.dest("dist")); }); تنبيه: احرص على استدعاء babel()‎ قبل إجراء أية عملية تصغير أو ضغط على شيفرة JavaScript. تحسين الصور أظن أنك ستتوقع أننا سنحتاج إلى تثبيت إضافة لمساعدتنا في موضوع تحسين الصور، وهي gulp-imagemin. الضغط الذي ستوفره هذه الإضافة هو الضغط غير الفَقُود (lossless compression). npm install gulp-imagemin –save-dev يمكننا تصغير صورة png و jpg و gif وحتى svg باستخدام إضافة gulp-imagemin. لننشِئ مهمّة images لهذا الغرض: var imagemin = require('gulp-imagemin'); gulp.task('images', function(){ return gulp.src('app/images/**/*.+(png|jpg|gif|svg)') .pipe(imagemin()) .pipe(gulp.dest('dist/images')) }); ولأن كلُّ نوعٍ من أنواع الصور سيُحسّن بطريقةٍ مختلفة، فربما ترغب بإضافة بعض الخيارات إلى imagemin لتخصيص كيفية تحسين الصورة. على سبيل المثال، يمكننا إنشاء صورة GIF متداخلة (interlaced) بضبط قيمة الخيار interlaced إلى ture. gulp.task('images', function(){ return gulp.src('app/images/**/*.+(png|jpg|jpeg|gif|svg)') .pipe(imagemin({ interlaced: true })) .pipe(gulp.dest('dist/images')) }); إن كانت تريد إجراء عملية ضغط فقود (lossy compression)، والذي سيخفّض حجم الصورة بشكل كبيرة على حساب الجودة، فاستعمل الإضافة imagemin-pngquant. npm install imagemin-pngquant –save-dev خيار الضغط الذي توفره هذه الإضافة يتمتع بالذكاء والذي يدعى PNG quantization (توضيح صور PNG)، إذ يعمل على اللعب بالألوان التي يراها دماغنا على أنها تقريبًا متماثلة محولًا الصورة إلى حجم 256 أو 8 بت للألوان. أفضل شيء في هذه الإضافة هو أنها لن تعدل أي شيء على الصور إن لم تتحقق حدود معينة متعلقة بالجودة. سنعدل على المثال السابق لاستعمال pngquant()‎ مع imagemin بالشكل التالي: var imagemin = require('gulp-imagemin'); gulp.task('default', function() { return gulp.src('app/images/*') .pipe(imagemin({ progressive: true, use: [pngquant()] })) .pipe(gulp.dest('dist/images')); }); على أي حال، عملية تحسين الصور هي عمليةٌ بطيئةٌ للغاية، ولا ترغب في تكرارها إلا إذا كان ذلك ضروريًا، ويمكننا تخزينها مؤقتًا باستخدام إضافة gulp-cache. npm install gulp-cache –save-dev الشيفرة التي سنضيفها إلى ملف gulpfile.js: var cache = require('gulp-cache'); gulp.task('images', function(){ return gulp.src('app/images/**/*.+(png|jpg|jpeg|gif|svg)') // تخزين الصور المُحسّنة مؤقتًا .pipe(cache(imagemin({ interlaced: true }))) .pipe(gulp.dest('dist/images')) }); انتهينا تقريبًا من عمليات التحسين، وبقي مجلدٌ أخير يجب علينا نقله من مجلد app إلى dist وهو مجلد الخطوط. لمزيد من التفاصيل حول تحسين الصور في موقعك، ننصحك بالإطلاع على مقال «دليلك الشامل لتحسين أداء الصور على موقعك». نسخ الخطوط إلى مجلد dist لن تحتاج الخطوط إلى أيّة عمليات تحسين، وكل ما علينا فعله هو نسخها إلى مجلد dist. يمكننا نسخ الملفات باستخدام Gulp ببساطة باستخدام الدالتين gulp.src و gulp.dest دون أيّة إضافات: gulp.task('fonts', function() { return gulp.src('app/fonts/**/*') .pipe(gulp.dest('dist/fonts')) }) سينسخ Gulp مجلد fonts من app إلى dist في كل مرة تشغِّل فيها الأمر gulp fonts. أصبحت لدينا ست مهام مختلفة في ملف gulpfile.js، ويجب علينا استدعاؤها يدويًا باستخدام سطر الأوامر. ربما نفكّر بربط المهام جميعًا إلى أمرٍ وحيد، لكن قبل ذلك لننظر إلى كيفية حذف الملفات المولّدة تلقائيًا. حذف الملفات المولدة تلقائيًا لمّا كنّا نولِّد الملفات تلقائيًا، فعلينا أن نتأكد أنَّ الملفات التي لم تعد مستخدمةً لن تبقَ موجودةً دون علمنا. سنحتاج إلى استخدام del لمساعدتنا في ذلك. npm install del –save-dev الدالة del تأخذ مصفوفةً من محارف التحديد (globs) التي تخبرها ما هي المجلدات التي يجب حذفها. يمكننا استخدامها كمهمّة في Gulp كما فعلنا من قبل: var del = require('del'); gulp.task('clean:dist', function() { return del.sync('dist'); }) سيَحذف Gulp المجلد dist في كل مرة تُشغِّل فيها الأمر gulp clean:dist. ملاحظة: لا حاجة أن نقلق من حذف مجلد dist/images لأنَّ إضافة gulp-cache خزَّنت نسخةً مؤقتةً من الصور في نظامك. ستحتاج إلى إنشاء مهمّة منفصلة لحذف النسخة المؤقتة وليكن اسمها cache:clear: gulp.task('cache:clear', function (callback) { return cache.clearAll(callback) }) جمع مهام Gulp مع بعضها ما فعلناه إلى الآن هو إنشاء مجموعتين منفصلتين من مهمات Gulp. الغرض من المجموعة الأولى هو المساعدة في التطوير، حيث حوّلنا ملفات Sass إلى CSS، وراقبنا تغيرات الملفات، وأعدنا تحميل الصفحة في متصفح الويب وقت الحاجة. أما المجموعة الثانية فكانت لتحسين الملفات الملحقة بالصفحة، حيث جهّزنا جميع الملفات للموقع الإنتاجي، وذلك بدمج ملفات CSS و JavaScript وتصغيرها، وتحسين الصور ونسخ الخطوط من app إلى dist. لقد وضعنا أول مجموعة من المهمات في بُنية يمكن تشغيلها باستخدام الأمر gulp watch: gulp.task('watch', ['browserSync', 'sass'], function (){ // ... }) أما المجموعة الثانية فتستخدم لإنشاء الموقع الإنتاجي، وهي تتضمن المهمات clean:dist و sass و useref و images و fonts. يمكننا إنشاء مهمة باسم build التي ستجمع كل ما سبق في مهمة واحدة. gulp.task('build', [`clean`, `sass`, `useref`, `images`, `fonts`], function (){ console.log('Building files'); }) للأسف لا يمكننا كتابة المهمّة build بهذه الطريقة لأن Gulp سيشغِّل المهمات المُمرَّرة كوسيطٍ ثانٍ إلى الدالة task معًا وليس بالترتيب. فهنالك احتمالٌ أن ينتهي تنفيذ المهمات useref أو images أو حتى fonts قبل إكمال المهمّة clean التي تُسبِّب حذف كامل مجلد dist! لذا لضمان أنَّ المهمات ستُنفَّذ بترتيبٍ صحيح، فسنحتاج إلى استخدام إضافة خارجية باسم Run Sequence: npm install run-sequence –save-dev وهذه هي البنية العامة لطريقة تشغيل سلسلة من المهمّات: var runSequence = require('run-sequence'); gulp.task('task-name', function(callback) { runSequence('task-one', 'task-two', 'task-three', callback); }); عندما تستدعى المهمّة task-name فسيُشغِّل Gulp المهمّة task-one أولًا، وبعد انتهاء تنفيذها سيُشغِّل المهمة task-two، ثم task-three وهكذا. تسمح لك إضافة Run Sequence بتشغيل المهمات معًا إذا وضعتَها في مصفوفة: gulp.task('task-name', function(callback) { runSequence('task-one', ['tasks','two','run','in','parallel'], 'task-three', callback); }); في هذه الحالة، سيُشغِّل Gulp المهمّة task-one، وبعد انتهاء تنفيذها فسيُشغِّل جميع المهمات الموجودة في المصفوفة معًا، وجميع المهمات الموجودة في المصفوفة يجب أن ينتهي تنفيذها قبل تنفيذ task-three. لذا يمكننا الآن إنشاء مهمة التي تُشغِّل المهمّة clean:dist أولًا، ثم تتبعها بقية المهمات: gulp.task('build', function (callback) { runSequence('clean:dist', ['sass', 'useref', 'images', 'fonts'], callback ) }) ولجعل الملف متناسقًا، فسنستخدم نفس الطريقة مع أوّل مجموعة من المهمات، وسنستخدم الاسم default كاسمٍ للمهمّة: gulp.task('default', function (callback) { runSequence(['sass','browserSync', 'watch'], callback ) }) لماذا اخترنا الاسم default؟ لأنّ المهمّة ذات الاسم default ستُشغَّل إن كَتبتَ الأمر gulp دون تمرير اسم المهمة له، مما يوفِّر عليك بعض الوقت :-) . الخلاصة بدأنا درسنا ونحن لا نعرف شيئًا عن Gulp، ثم كتبنا مهمةً بسيطةً لتحويل ملفات Sass إلى CSS ومراقبة ملفات HTML و JS، وتعلمنا كيف نشغِّل تلك المهمّة في سطر الأوامر باستخدام الأمر gulp. ثم بنينا بعد ذلك مهمّةً ثانيةً، ألا وهي build، والتي تُنشِئ المجلد dist لكي يحتوي على ملفات الموقع الإنتاجي، وحوّلنا ملفات Sass إلى CSS وأجرينا عمليات تحسين على ملحقات الصفحة، ونسخنا المجلدات الضرورية إلى مجلد dist؛ وتمكنّا من تنفيذ كل ما سبق بأمرٍ وحيد وهو gulp build. في النهاية، أنشأنا المهمّة clean التي تحذف المجلد dist وبالتالي سنحذف أيّة ملفات غير مستخدمة في الموقع. لا تقف عند هذا الحد! Gulp واسع جدًا وإمكانياته لا تنتهي، وأرى أنَّ عليك تصفّح الإضافات الخاصة به والموجودة في الموقع الرسمي، وعليك الاستعانة بتوثيق API إن أردت رؤية توثيق دوال Gulp. المصادر تعتمد هذه المقالة اعتمادًا أساسيًا على مقالة Gulp for Beginners لصاحبها Zell Liew قائمة بالمقالات التي تتحدث عن Gulp المجاري (streams) في Node مقالة Automate Your Tasks Easily with Gulp.js لصاحبها Justin Rexroad
  3. أداة البناء Grunt أو Gulp، مكتبة require.js، ‏browserify، الإصدار السادس من ES، المفسرات، أطر عمل React و Angular و Amber، التعابير المغلقة (closures)، سلسلة prototype. ارتفاع في ضغط الدم يؤدي إلى سكتة دماغية. حسنًا، تطوير الويب أمرٌ ممتع جدًا، لكن JavaScript مروعة! تجد نفسك منسجمًا تمامًا مع جميع جوانب تطوير الويب، لكن عندما يأتي الأمر إلى JavaScript فستشعر أنَّ جزءًا كبيرًا من المعلومات الأساسية ينقصك بينما يعرفه الآخرون، والذي سيؤدي إلى جعلهم يفهمون سكربتات JavaScript. نعم، الحقيقة هي أنَّك تفتقد بالفعل إلى بعض القطع؛ لكن هذا لا يعني أنَّ التوجه الحالي لتطوير الواجهات الأمامية ليس مجنونًا! اطمئن، فأنت لستَ بمفردك، لذا اسحب كرسيًا واجلس، وجهِّز نفسك لكتابة تطبيق JavaScript. أوّل خطوة هي ضبط بيئة التطوير المحلية، لذا اتخذ قرارك: هل ستستخدم Gulp، أم Grunt، لا! سأستعمل سكربتات NPM. هل أستعمل Webpack أم Browserify أم Require.js؟ هل أتخذ قرارًا مصيريًا بالانتقال إلى الإصدار السادس من ES؟ أليس ضروريًا أن أضع مرجعًا عن أمراض القلب بجواري؟ كيف سأنظِّم اختبار الشيفرات؟ هل من إطارِ عملٍ تنصحني به؟ أليس من الأفضل تشغيل الاختبارات من سطر الأوامر، لنستعمل إذًا PhantomJS؟ مع أي إطارٍ أذهب: Angular أم React؟ ربما Ember؟ ماذا عن Backbone؟ ربما قرأتَ بعض صفحات توثيق React ووجدتَ فيها أنَّ «Redux هو حاويةٌ ذاتُ حالةٍ قابلةٍ للتوقع لتطبيقات JavaScript» وبدت على وجهك أمارات الرضى، فمن المؤكد أنَّك ستحتاج إلى هذه الميزة العظيمة، بغض النظر عن أنَّك لم تفهم حرفًا من شرحها. السؤال الآن هو: لماذا أصبح تطوير تطبيقات JavaScript أمرًا يدفع إلى الجنون؟! دعني أساعدك لفهم سبب ذلك. لنبدأ بمثالٍ بسيطٍ ثم سنستعرض صورًا جميلةً توضِّح وجهة نظري. هذا تطبيق «Hello, World!‎» مكتوبٌ باستخدام React: // main.js var React = require('react'); var ReactDOM = require('react-dom'); ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('example') ); لم ننتهِ منه بعد: $ npm install --save react react-dom babelify babel-preset-react $ browserify -t [ babelify --presets [ react ] ] main.js -o bundle.js هنالك عدِّة خطوات ناقصة هنا، مثل تثبيت مكتبة browserify أو ما الذي عليك فعله لتشغيل الصفحة في المتصفح، إذ لا يبدو أنَّ ما سبق سيُنشِئ صفحة ويب قادرة على فعل أيّ شيء! بعد أن تنتهي من إنجاز ما سبق، فستجد ملفًا يدعى bundle.js يحتوي على تطبيق «Hello, World!‎» السابق المكتوب بمكتبة React والذي يضم حوالي 19374 سطرًا برمجيًا، وكل ما فعلتَه هو تثبيت browserify و babelify و react-dom، التي «تزن» آلاف الأسطر البرمجية. هذه صورة تعبيرية عن برنامج «Hello, World!‎» في React: حسنًا، هذا تطبيق «Hello, World!‎» باستخدام JavaScript دون مكتبات: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>Hello World</title> </head> <body> <div id="container"></div> <script> document.body.onload = function(){ var container = document.getElementById("container"); container.innerHTML = '<h1>"Hello, world!"</h1>'; } </script> </body> </html> هذا كل ما في الأمر! 18 سطر برمجي (يمكن اختصارها إلى أقل من ذلك)، التي تستطيع نسخها ولصقها في ملفٍ باسم index.html وتنقر نقرتين عليه مما يفتحه في متصفحك. يا للبساطة! إذا كنتَ تفكِّر في هذه اللحظة «أليس إطار React يفعل أكثر من ذاك المثال البسيط الذي كتبته، والذي لا يرقى أن يكون تطبيق JavaScript» فأنت مصيبٌ (تقريبًا)، وعلى بعد خطوة واحدة من فهمك لماذا كل هذا التعقيد. انظر إلى هذه الصورة: أغلبية تطبيقات JavaScript التي ستعمل عليها ستقع في مكانٍ ما في منتصف المنحني الجرسي (bell curve) السابق. وإذا كنتَ في منتصف المنحني السابق وبدأت تطبيقًا بالاعتماد على React فسوف ينتهي بك المطاف بهندسة تطبيقك زيادةً عن اللزوم من بدايته. وهذا هو سبب تعقيد تطوير تطبيقات JavaScript، لأنَّ غرض أغلبية الأدوات التي تظن أنَّك بحاجةٍ إليها هو حلّ المشاكل التي لن تتعرض إليها بتاتًا. أصبحت حالة تطوير تطبيقات JavaScript في الآونة الأخيرة معقدةً ومربكةً لأنَّ الجميع يبالغون في هندسة تطبيقاتهم دون أن يدركوا ذلك. إذًا، كيف يجب أن نبدأ بتطوير تطبيق JavaScript؟ هل علينا استخدام مكتبة شبيهة بمكتبة React أو Angular؟ هل يجب أن نستخدم مدير للحزم؟ ماذا يفترض علينا أن نفعل إذا لم نستخدمهما؟ هل كتابة الاختبارات ضرورية؟ هل علينا أصلًا توليد شيفرات HTML عبر JavaScript؟ هذه هي الأسئلة التي يجب أن تسألها لنفسك قبل أن تبدأ بمجموعة ضخمة من أدوات التطوير. عندما تبدأ بتطوير تطبيق JavaScript فمن المهم أن تختار نقطةً في المنحني الجرسي في المكان الذي تظن أنَّ من المرجح أن يصله تطبيقك في المستقبل من ناحية التعقيد. لن أكذب عليك، فعل ذلك ليس سهلًا ويحتاج خبرةً، لكن هنالك منطقةٌ كبيرة يمكنك أن تبدأ منها أغلبية تطبيقات JavaScript: استعمل مكتبة jQuery مع قوالب لصفحات الواجهة الأمامية مع أداة بناء بسيطة لجمع الملفات وتصغيرها (بفرض أنَّ إطار العمل الذي تستعمله لتطوير السند الخلفي [backend] لا يفعل ذلك تلقائيًا). إذا أردتَ أن تتعلم كيفية هيكلة تطبيق JavaScript بطريقةٍ صحيحة، فعليك أن تبدأ بفهم كيف ومتى ولماذا تستخدم إطار عمل أو حزمة npm أو إصدار ES6 أو متى تكتب اختبارات أو هل عليك جعل الاختبارات تعمل محليًا أو في متصفح، ثم سيأتي دور بقية الأسئلة وحلّ بقية المشاكل. إن أردتَ أن تملأ الفجوات الموجودة في معلوماتك حول تطوير JavaScript وأن تتجنّب الشعور بأنّك تبالغ في تصميم تطبيق JavaScript فحاول أن تتابع ما نطرحه هنا في قسم البرمجة في أكاديمية حسوب. ترجمة -وبتصرّف- للمقال Why JavaScript Development is Crazy لصاحبه Sean Fioritto