اذهب إلى المحتوى

أيهما أفضل كأداة مساعدة Webpack أم Browserify مع Gulp؟


Mohamed Lahlah

إن تزايد تعقيد تطبيقات الوِب في وقتنا الحالي جعل من ضرورة زيادة قابلية تطبيقات الوِب للتوسعة والتطوير أمرًا في غاية الأهمية. وعلى الرغم من أن الحلول القديمة المخصصة لكتابة شيفرات جافاسكربت و jQuery كانت فعَالة وكافية إلى حدٍ ما، ولكن بناء تطبيق وِبٍ في وقتنا الحاضر يتطلب درجة كبير من الانضباط والمنهجية الرسمية في تطوير البرمجيات. وهذه بعض الأمثلة على الممارسات الجيدة في تطوير البرمجيات:

  • استخدام اختبارات الوحدة (Unit tests) وذلك للتأكد من أن تعديلات ما على الشيفرة البرمجية لن يعطّل شيفرة أُخرى.
  • استخدام عملية كشف الأخطاء المحتملة (Linting) لضمان كتابة شيفرة برمجية خالية من الأخطاء.
  • استخدام طرق مختلفة للبنية الهيكلية للشيفرة البرمجية لكلّ من وضع التطوير ووضع النشر.

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

1.png

من الشائع في وقتنا الحالي استخدام لغات ذات معالجة مُسبّقة (language preprocessors) مثل: SASS و JSX والّتي تُترجم من هذه اللغات إلى ملفات جافاسكربت وملفات تنسيقات. بالإضافة إلى ذلك شاع أيضًا استخدام المحوّل (Transpilers) وهو محوّل يغيّر الشيفرة البرمجية من إصدار معين للغة جافاسكربت إلى إصدار أقدم، وذلك لضمان عمل هذه الشيفرة البرمجية على جميع المتصفحات. مثل المحوّل Babel.

إن هذه المهام (والّتي لا علاقة لها بالمنطق البرمجي لتطبيق الوِب بحد ذاته) تمثل عبئًا على على المطور. ولكن لحسن الحظ يوجد حلٌ لهذا الأمر وهو منفذ المهام (Task Runner) والّذي جاء لمساعدتنا في أتمتة هذه المهام حتى نستطيع أن نحسّن من بيئة التطوير مع التركيز على بناء المنطق البرمجي لتطبيق الوِب. بمجرد ضبط الإعدادات الخاصة بمنفذ المهام كلّ ما عليك فعله هو استدعاء أمرٍ على الطرفية وستُنفذ بعدها المهام المؤتمتة.

سأستخدم الأداة Gulp كمنفذ للمهام وذلك لأنه مناسب للمبرمجين وسهل التعلّم ومفهوم (تحدثنا في مقال سابق كيفية استخدام هذه الأداة بكلّ التفاصيل).

مقدمة سريعة لأداة Gulp

تتألف واجهة برمجة التطبيقات (API) لهذه الأداة من أربع دوالّ:

  • gulp.src
  • gulp.dest
  • gulp.task
  • gulp.watch

2.png

في المثال التالي نلاحظ أن المهمة 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؟

3.png

عندما نستخدام نمط 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) سواءً كمهمة قائمة بحد ذاتها أو في وضع المراقبة.
  • توفير القدرة على جمع كلّ المهام السابقة عبر أمر واحد لكتابته عبر الطرفية.
  • وجود أمر آخر لتجميع الملفات وضغطها أو تنفيذ تحسينات أخرى عليها من أجل عملية النشر.

وستكون طرق إعداد منفذ المهام على الشكل التالي:

  1. Gulp + Browserify
  2. Gulp + Webpack
  3. 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.

محزم ملفات التنسيق

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":

اقتباس

كنت من أشد المعجبين بأداة Gulp، ولكن في مشروعي الأخير أنتهى بي المطاف بكتابة مئات الأسطر من الشيفرات البرمجية في ملف الإعداد gulpfile واستخدام حوالي 12 مُلحق من ملحقات Gulp. كما أنني كافحت من أجل دمج Webpack مع Browsersync مع hot reloading مع Mocha والكثير من الشيفرات Gulp. ولكن لماذا توجهت إلى NPM Scripts؟ لأن بعض التوثيقات الرسمية للملحقات لم تحتوي على المعلومات الكافية الّتي ستُغطي طريقة استخدامي لها في مشروعي. كما كشفت بعض الملحقات عن جزء من واجهة برمجة التطبيقات (API) الّتي احتجتها. ومن مكان أخر تظهر مشكلة غريبة والّذي من المفترض أن لا أشاهد سوى ملفات خفيفة وبسيطة. بالإضافة إلى ذلك أزيلت الألون المخصصة للأوامر عندما عملت مع سطر الأوامر.

يحدد كوري هاوس ثلاث مشاكل رئيسية وهي:

  1. مشكلة الاعتماد على منشئي الملحقات.
  2. مشكلة تنقيح الأخطاء المرهقة.
  3. مشكلة التوثيقات الرسمية المفككة والضعيفة.

ومن وجهة نظري أتفق مع جميع هذه المشاكل.

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


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...