Webpack أو Browserify & Gulp: أيهما أفضل؟
نشرت: 2022-03-11نظرًا لأن تطبيقات الويب تزداد تعقيدًا ، فإن جعل تطبيق الويب الخاص بك قابلاً للتطوير يصبح ذا أهمية قصوى. في حين أن كتابة JavaScript و jQuery المخصصة في الماضي كانت كافية ، فإن إنشاء تطبيق ويب في الوقت الحاضر يتطلب درجة أكبر بكثير من الانضباط وممارسات تطوير البرامج الرسمية ، مثل:
- اختبارات الوحدة للتأكد من أن التعديلات التي تم إجراؤها على التعليمات البرمجية لا تؤدي إلى تعطيل الوظائف الحالية
- الفحص لضمان أسلوب تشفير متسق خالٍ من الأخطاء
- بنيات الإنتاج التي تختلف عن بنيات التطوير
يوفر الويب أيضًا بعضًا من تحديات التنمية الفريدة الخاصة به. على سبيل المثال ، نظرًا لأن صفحات الويب تقدم الكثير من الطلبات غير المتزامنة ، يمكن أن يتدهور أداء تطبيق الويب الخاص بك بشكل كبير من الاضطرار إلى طلب مئات من ملفات JS و CSS ، ولكل منها أعباء صغيرة خاصة بها (الرؤوس والمصافحة وما إلى ذلك). غالبًا ما يمكن معالجة هذه المشكلة بالذات عن طريق تجميع الملفات معًا ، لذلك فأنت تطلب فقط ملف JS و CSS مجمع واحد بدلاً من مئات الملفات الفردية.
من الشائع أيضًا استخدام معالجات اللغة التمهيدية مثل SASS و JSX التي يتم تجميعها إلى JS و CSS الأصليين ، بالإضافة إلى محولات JS مثل Babel ، للاستفادة من كود ES6 مع الحفاظ على توافق ES5.
يرقى هذا إلى عدد كبير من المهام التي لا علاقة لها بكتابة منطق تطبيق الويب نفسه. هذا هو المكان الذي يأتي فيه متسابقوا المهام. الغرض من عداء المهام هو أتمتة جميع هذه المهام بحيث يمكنك الاستفادة من بيئة التطوير المحسّنة مع التركيز على كتابة تطبيقك. بمجرد تكوين عداء المهام ، كل ما عليك فعله هو استدعاء أمر واحد في المحطة.
سأستخدم Gulp باعتباره عداءًا للمهام لأنه سهل للغاية للمطورين ، وسهل التعلم ، ومفهوم بسهولة.
مقدمة سريعة لجلب
تتكون واجهة برمجة تطبيقات Gulp من أربع وظائف:
-
gulp.src -
gulp.dest -
gulp.task -
gulp.watch
هنا ، على سبيل المثال ، نموذج لمهمة تستخدم ثلاث من هذه الوظائف الأربع:
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 /public/js/**/*.js ثم نقلها إلى مجلد build .
يكمن جمال هذا في تسلسل .pipe() . تأخذ مجموعة من ملفات الإدخال ، وتوجهها عبر سلسلة من التحويلات ، ثم تعيد ملفات الإخراج. لجعل الأمور أكثر ملاءمة ، غالبًا ما يتم إجراء تحويلات الأنابيب الفعلية ، مثل minify() ، بواسطة مكتبات NPM. نتيجة لذلك ، من النادر جدًا في الممارسة العملية أن تحتاج إلى كتابة تحويلاتك الخاصة بخلاف إعادة تسمية الملفات في الأنبوب.
الخطوة التالية لفهم Gulp هي فهم مجموعة تبعيات المهام.
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... }); هنا ، تقوم my-second-task بتشغيل وظيفة رد الاتصال فقط بعد اكتمال مهام lint bundle . يسمح هذا بفصل الاهتمامات: يمكنك إنشاء سلسلة من المهام الصغيرة بمسؤولية واحدة ، مثل تحويل LESS إلى CSS ، وإنشاء نوع من المهام الرئيسية التي تستدعي ببساطة جميع المهام الأخرى عبر مجموعة تبعيات المهام.
أخيرًا ، لدينا gulp.watch ، الذي يراقب التغييرات في نمط ملف glob ، وعندما يتم الكشف عن تغيير ، يتم تشغيل سلسلة من المهام.
gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) }) في المثال أعلاه ، ستؤدي أي تغييرات على ملف مطابق /public/js/**/*.js إلى تشغيل مهمة lint وإعادة reload . الاستخدام الشائع لـ gulp.watch هو تشغيل عمليات إعادة التحميل الحية في المتصفح ، وهي ميزة مفيدة جدًا للتطوير بحيث لن تتمكن من العيش بدونها بمجرد تجربتها.
وبهذه الطريقة ، أنت تفهم كل ما تحتاج حقًا لمعرفته حول gulp .
أين تناسب Webpack؟
عند استخدام نمط CommonJS ، فإن تجميع ملفات JavaScript ليس بسيطًا مثل تجميعها. بدلاً من ذلك ، لديك نقطة require (تسمى عادةً index.js أو app.js ) مع سلسلة من عبارات الطلب أو 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 ، وقد يكون لتلك التبعيات نفسها المزيد من التبعيات لحلها. علاوة على ذلك ، قد require إلى نفس التبعية في أماكن متعددة في التطبيق الخاص بك ، لكنك تريد فقط حل هذه التبعية مرة واحدة. كما يمكنك أن تتخيل ، بمجرد أن يكون لديك شجرة تبعية على عمق بضعة مستويات ، تصبح عملية تجميع JavaScript الخاصة بك معقدة نوعًا ما. هذا هو المكان الذي تأتي فيه حزم مثل Browserify و Webpack.
لماذا يستخدم المطورون Webpack بدلاً من Gulp؟
Webpack عبارة عن أداة تجميع بينما Gulp عبارة عن عداء مهام ، لذلك تتوقع أن ترى هاتين الأداتين تستخدمان بشكل شائع معًا. بدلاً من ذلك ، هناك اتجاه متزايد ، خاصة بين مجتمع React ، لاستخدام Webpack بدلاً من Gulp. لماذا هذا؟
ببساطة ، يعد Webpack أداة قوية يمكنها بالفعل أداء الغالبية العظمى من المهام التي كنت تقوم بها بطريقة أخرى من خلال عداء المهام. على سبيل المثال ، يوفر Webpack بالفعل خيارات للتصغير وخرائط المصادر لحزمتك. بالإضافة إلى ذلك ، يمكن تشغيل Webpack كبرنامج وسيط من خلال خادم مخصص يسمى webpack-dev-server ، والذي يدعم كلاً من إعادة التحميل المباشر وإعادة التحميل السريع (سنتحدث عن هذه الميزات لاحقًا). باستخدام اللوادر ، يمكنك أيضًا إضافة ES6 إلى نقل ES5 ومعالجات CSS قبل وبعد. هذا في الحقيقة يترك فقط اختبارات الوحدة والفحص كمهام رئيسية لا يمكن لـ Webpack التعامل معها بشكل مستقل. نظرًا لأننا قمنا بتخفيض ما لا يقل عن نصف دزينة من مهام gulp المحتملة إلى اثنتين ، فإن العديد من المطورين يختارون بدلاً من ذلك استخدام البرامج النصية NPM مباشرةً ، لأن هذا يتجنب النفقات العامة لإضافة Gulp إلى المشروع (والذي سنتحدث عنه أيضًا لاحقًا) .
العيب الرئيسي لاستخدام Webpack هو أنه من الصعب تكوينه ، وهو أمر غير جذاب إذا كنت تحاول إنشاء مشروع وتشغيله بسرعة.
لدينا 3 إعدادات عداء المهام
سوف أقوم بإعداد مشروع بثلاثة إعدادات مختلفة لتشغيل المهام. سيقوم كل إعداد بتنفيذ المهام التالية:
- قم بإعداد خادم تطوير مع إعادة التحميل المباشر عند حدوث تغييرات في الملف المرصود
- قم بتجميع ملفات JS & CSS الخاصة بنا (بما في ذلك ES6 إلى ES5 transpilation ، وتحويل SASS إلى CSS وخرائط المصادر) بطريقة قابلة للتطوير على تغييرات الملفات التي تمت مراقبتها
- قم بتشغيل اختبارات الوحدة إما كمهمة قائمة بذاتها أو في وضع المراقبة
- قم بتشغيل الفحص إما كمهمة قائمة بذاتها أو في وضع المشاهدة
- وفر القدرة على تنفيذ كل ما سبق عبر أمر واحد في الجهاز
- لديك أمر آخر لإنشاء حزمة إنتاج مع التصغير والتحسينات الأخرى
ستكون إعداداتنا الثلاثة:
- المتصفح + Gulp
- Gulp + Webpack
- البرامج النصية Webpack + NPM
سيستخدم التطبيق 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.jsindex.html
ملف HTML مباشر. يتم تحميل تطبيق React في <div></div> ونحن نستخدم فقط ملف JS و CSS مجمع واحد. في الواقع ، في إعداد تطوير Webpack الخاص بنا ، لن نحتاج حتى إلى bundle.css .
index.js
هذا بمثابة نقطة دخول JS لتطبيقنا. بشكل أساسي ، نقوم فقط بتحميل React Router في div مع app id الذي ذكرناه سابقًا.
طرق. js
هذا الملف يحدد مساراتنا. يتم تعيين عناوين url / و /about و /contact إلى مكونات الصفحة AboutPage HomePage ContactPage ، على التوالي.
index.test.js
هذه سلسلة من اختبارات الوحدة تختبر سلوك JavaScript الأصلي. في تطبيق جودة إنتاج حقيقي ، ستكتب اختبار وحدة لكل مكون React (على الأقل تلك التي تتلاعب بالحالة) ، وتختبر السلوك الخاص بـ React. ومع ذلك ، لأغراض هذا المنشور ، يكفي ببساطة أن يكون لديك اختبار وحدة وظيفية يمكن تشغيله في وضع المشاهدة.
المكونات / App.js
يمكن اعتبار هذا بمثابة الحاوية لجميع مشاهدات الصفحة الخاصة بنا. تحتوي كل صفحة على مكون <Header/> بالإضافة إلى this.props.children ، والذي يتم تقييمه لعرض الصفحة نفسه (على سبيل المثال / ContactPage if at /contact في المتصفح).
المكونات / الصفحة الرئيسية / HomePage.js
هذا هو عرض منزلنا. لقد اخترت استخدام react-bootstrap لأن نظام شبكة bootstrap ممتاز لإنشاء صفحات سريعة الاستجابة. مع الاستخدام الصحيح للتمهيد ، يتم تقليل عدد استعلامات الوسائط التي يجب أن تكتبها لإطارات العرض الأصغر بشكل كبير.
يتم تنظيم المكونات المتبقية ( Header و AboutPage و ContactPage ) بشكل مشابه (ترميز react-bootstrap تمهيد التشغيل ، ولا يوجد معالجة للحالة).
الآن دعنا نتحدث أكثر عن التصميم.
نهج التصميم CSS
أسلوبي المفضل لتصميم مكونات React هو الحصول على ورقة أنماط واحدة لكل مكون ، يتم تحديد أنماطها لتطبيقها على هذا المكون المحدد فقط. ستلاحظ أنه في كل مكون من مكونات React الخاصة بي ، يحتوي المستوى الأعلى div على اسم فئة يطابق اسم المكون. لذلك ، على سبيل المثال ، يتم تغليف ترميز HomePage.js بواسطة:
<div className="HomePage"> ... </div> يوجد أيضًا ملف HomePage.scss مرتبط منظم على النحو التالي:
@import '../../styles/variables'; .HomePage { // Content here }لماذا هذا النهج مفيد جدا؟ ينتج عنه CSS معياري للغاية ، مما يلغي إلى حد كبير مشكلة السلوك المتتالي غير المرغوب فيه.
افترض أن لدينا مكونين من مكونات React ، Component1 1 Component2 2. في كلتا الحالتين ، نريد تجاوز حجم خط h2 .
/* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } } يكون حجم خط h2 Component1 1 Component2 2 مستقلين سواء كانت المكونات متجاورة أو كان أحد المكونات متداخلاً داخل الآخر. من الناحية المثالية ، يعني هذا أن تصميم المكون مستقل تمامًا ، مما يعني أن المكون سيبدو كما هو تمامًا بغض النظر عن مكان وضعه في العلامات الخاصة بك. في الواقع ، ليس الأمر بهذه البساطة دائمًا ، لكنه بالتأكيد خطوة كبيرة في الاتجاه الصحيح.
بالإضافة إلى الأنماط لكل مكون ، أود أن يكون لدي مجلد styles يحتوي على ورقة أنماط عالمية global.scss ، جنبًا إلى جنب مع أجزاء SASS التي تتولى مسؤولية معينة (في هذه الحالة ، _fonts.scss و _variables.scss للخطوط والمتغيرات ، على التوالي ). تتيح لنا ورقة الأنماط العامة تحديد الشكل العام والمظهر للتطبيق بأكمله ، بينما يمكن استيراد الأجزاء المساعدة بواسطة أوراق الأنماط لكل مكون حسب الحاجة.
الآن وقد تم استكشاف الكود المشترك في كل فرع بعمق ، دعنا نحول تركيزنا إلى نهج عداء / تجميع المهام الأول.
إعداد Gulp + Browserify
gulpfile.js
يأتي هذا إلى ملف gulp كبير بشكل مدهش ، مع 22 عملية استيراد و 150 سطرًا من التعليمات البرمجية. لذلك ، من أجل الإيجاز ، سأراجع فقط المهام js و css server watch والمهام default بالتفصيل.
حزمة JS
// Browserify specific configuration 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); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. 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)); } هذا النهج قبيح إلى حد ما لعدد من الأسباب. لسبب واحد ، يتم تقسيم المهمة إلى ثلاثة أجزاء منفصلة. أولاً ، تقوم بإنشاء كائن حزمة b الخاص بك ، وتمرير بعض الخيارات وتحديد بعض معالجات الأحداث. ثم لديك مهمة Gulp نفسها ، والتي يجب أن تمرر وظيفة مسماة على أنها رد نداء بدلاً من تضمينها (حيث أن b.on('update') يستخدم نفس رد الاتصال). هذا بالكاد يتمتع بأناقة مهمة Gulp حيث تقوم فقط بتمرير gulp.src وتوجيه بعض التغييرات.
هناك مشكلة أخرى وهي أن هذا يجبرنا على اتباع طرق مختلفة لإعادة تحميل html و css و js في المتصفح. النظر إلى مهمة watch Watch الخاصة بنا:
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. عندما يتم تغيير ملف CSS ، تتم إعادة تشغيل مهمة css ، ويؤدي الأنبوب الأخير في مهمة css إلى تشغيل livereload() وتحديث المتصفح.
ومع ذلك ، فإن js watch لا تستدعي مهمة js على الإطلاق. بدلاً من ذلك ، يعالج معالج الأحداث في b.on('update', bundle) إعادة التحميل باستخدام نهج مختلف تمامًا (أي استبدال الوحدة الساخنة). التناقض في هذا النهج مزعج ، ولكنه ضروري للأسف من أجل الحصول على عمليات بناء تدريجية . إذا أطلقنا بسذاجة اسم livereload() في نهاية وظيفة bundle ، فسيؤدي ذلك إلى إعادة بناء حزمة JS بأكملها على أي تغيير فردي لملف JS. من الواضح أن مثل هذا النهج لا يتسع نطاقه. كلما زاد عدد ملفات JS لديك ، زادت مدة كل عملية إعادة تجميع. فجأة ، تبدأ عمليات إعادة تجميع 500 مللي ثانية في أخذ 30 ثانية ، مما يعيق التطور السريع حقًا.
حزمة CSS
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())); }); المشكلة الأولى هنا هي تضمين CSS للمورد المرهق. عندما يتم إضافة ملف CSS جديد للبائع إلى المشروع ، علينا أن نتذكر تغيير ملف gulpfile الخاص بنا لإضافة عنصر إلى مصفوفة gulp.src ، بدلاً من إضافة الاستيراد إلى مكان ذي صلة في شفرة المصدر الفعلية الخاصة بنا.
القضية الرئيسية الأخرى هي المنطق المعقد في كل أنبوب. اضطررت إلى إضافة مكتبة NPM تسمى gulp-cond فقط لإعداد المنطق الشرطي في الأنابيب الخاصة بي ، والنتيجة النهائية ليست سهلة القراءة (الأقواس الثلاثية في كل مكان!).
مهمة الخادم
gulp.task('server', () => { nodemon({ script: 'server.js' }); }); هذه المهمة مباشرة للغاية. إنه في الأساس غلاف حول استدعاء سطر الأوامر nodemon server.js ، والذي يقوم بتشغيل server.js في بيئة عقدة. يتم استخدام nodemon بدلاً من node بحيث تؤدي أي تغييرات على الملف إلى إعادة تشغيله. بشكل افتراضي ، nodemon بإعادة تشغيل عملية التشغيل على أي تغيير في ملف JS ، ولهذا السبب من المهم تضمين ملف 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();يقوم هذا بتعيين الدليل الأساسي للخادم والمنفذ بناءً على بيئة العقدة ، وإنشاء مثيل لـ express.
app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir))); يضيف هذا connect-livereload (الضروري لإعداد إعادة التحميل المباشر) والبرمجيات الوسيطة الثابتة (ضرورية للتعامل مع أصولنا الثابتة).

app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); }); هذا مجرد مسار بسيط لواجهة برمجة التطبيقات. إذا انتقلت إلى localhost:3000/api/sample-route في المتصفح ، فسترى:
{ website: "Toptal", blogPost: true }في الخلفية الحقيقية ، سيكون لديك مجلد كامل مخصص لمسارات API ، وملفات منفصلة لتأسيس اتصالات قاعدة البيانات ، وما إلى ذلك. تم تضمين هذا المسار النموذجي فقط لإظهار أنه يمكننا بسهولة إنشاء واجهة خلفية أعلى الواجهة الأمامية التي أنشأناها.
app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); }); هذا مسار شامل ، بمعنى أنه بغض النظر عن عنوان url الذي تكتبه في المتصفح ، سيعيد الخادم صفحة index.html الوحيدة الخاصة بنا. عندئذٍ ، تقع على عاتق React Router مسؤولية حل مساراتنا من جانب العميل.
app.listen(port, () => { open(`http://localhost:${port}`); });يخبر هذا المثيل السريع بالاستماع إلى المنفذ الذي حددناه ، وفتح المتصفح في علامة تبويب جديدة على عنوان URL المحدد.
الشيء الوحيد الذي لا أحبه في إعداد الخادم حتى الآن هو:
app.use(require('connect-livereload')({port: 35729})); نظرًا لأننا نستخدم بالفعل gulp-livereload في ملف gulp الخاص بنا ، فإن هذا يجعل مكانين منفصلين حيث يجب استخدام تحميل الكبد.
الآن ، أخيرًا وليس آخرًا:
المهمة الافتراضية
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 الأصل ، لكن في الوقت الحالي علينا أن نترك هذا الأمر مع هذا التذمر البسيط في إعدادنا.
أبعد من ذلك ، هذا في الواقع أنيق جدًا. يتم تنفيذ إنشاء واستضافة تطبيقنا بالكامل في أمر واحد ، وفهم أي جزء من سير العمل بسيط مثل فحص مهمة فردية في تسلسل التشغيل. بالإضافة إلى ذلك ، يمكننا تقسيم التسلسل بأكمله إلى أجزاء أصغر للحصول على نهج أكثر دقة لإنشاء التطبيق واستضافته. على سبيل المثال ، يمكننا إعداد مهمة منفصلة تسمى validate تشغيل مهام lint test . أو يمكن أن يكون لدينا مهمة host تقوم بتشغيل server watch . هذه القدرة على تنظيم المهام قوية جدًا ، خاصةً مع زيادة حجم تطبيقك ويتطلب المزيد من المهام الآلية.
التطوير مقابل بناء الإنتاج
if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production'; باستخدام مكتبة yargs NPM ، يمكننا تزويد Gulp بأعلام سطر الأوامر. هنا أوعز إلى gulpfile بضبط بيئة العقدة على الإنتاج إذا تم تمرير --prod إلى gulp في الجهاز. ثم يتم استخدام متغير PROD الخاص بنا كشرط للتمييز بين سلوك التطوير والإنتاج في ملف gulpfile الخاص بنا. على سبيل المثال ، أحد الخيارات التي browserify إلى تكوين المتصفح الخاص بنا هو:
plugin: PROD ? [] : [hmr, watchify] يخبر هذا browserify بعدم استخدام أي مكونات إضافية في وضع الإنتاج ، واستخدام hmr و watchify الإضافات في بيئات أخرى.
يعد شرط PROD هذا مفيدًا جدًا لأنه يحفظنا من الاضطرار إلى كتابة ملف gulpfile منفصل للإنتاج والتطوير ، والذي سيحتوي في النهاية على الكثير من تكرار الكود. بدلاً من ذلك ، يمكننا القيام بأشياء مثل gulp --prod لتشغيل المهمة الافتراضية في الإنتاج ، أو gulp html --prod لتشغيل مهمة html فقط في الإنتاج. من ناحية أخرى ، رأينا سابقًا أن ملء خطوط أنابيب Gulp بعبارات مثل .pipe(cond(!PROD, livereload())) ليست الأكثر قابلية للقراءة. في النهاية ، إنها مسألة تفضيل ما إذا كنت تريد استخدام نهج المتغير المنطقي أو إعداد ملفين منفصلين من gulp.
الآن دعنا نرى ما يحدث عندما نستمر في استخدام Gulp كمشغل مهامنا ولكننا نستبدل Browserify بـ Webpack.
إعداد Gulp + Webpack
فجأة ، أصبح ملف gulpfile الخاص بنا 99 سطرًا فقط مع 12 عملية استيراد ، وهو ما يمثل انخفاضًا كبيرًا عن الإعداد السابق! إذا تحققنا من المهمة الافتراضية:
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });الآن يتطلب إعداد تطبيق الويب الكامل لدينا خمس مهام فقط بدلاً من تسعة ، وهو تحسن كبير.
بالإضافة إلى ذلك ، قمنا بإلغاء الحاجة إلى livereload . watch الآن هي:
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); }); هذا يعني أن مراقب الجلب الخاص بنا لا يتسبب في أي نوع من السلوك الارتدادي. كمكافأة إضافية ، لا نحتاج إلى نقل index.html من app إلى dist أو build بعد الآن.
وبالعودة إلى تركيزنا على تقليل المهام ، تم استبدال مهامنا html و css و js fonts بمهمة build واحدة:
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.config.js لتكوينه ، وأرسل الحزمة الناتجة إلى baseDir بنا (إما dist أو build ، اعتمادًا على node env).
دعنا نلقي نظرة على ملف تهيئة Webpack:
webpack.config.js
هذا ملف تكوين كبير جدًا ومخيف ، لذلك دعونا نشرح بعض الخصائص المهمة التي يتم تعيينها على كائن module.exports الخاص بنا.
devtool: PROD ? 'source-map' : 'eval-source-map',يعيّن هذا نوع خرائط المصادر التي سيستخدمها 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 الخاص بنقطة الدخول المتوقعة بالإضافة إلى نقطة دخول webpack-hot-middleware التي تُستخدم كجزء من إعداد إعادة تحميل الوحدة الساخنة.
output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' }, هذا هو المكان الذي سيتم فيه إخراج الحزمة المترجمة. الخيار الأكثر إرباكًا هو publicPath . يقوم بتعيين عنوان url الأساسي للمكان الذي سيتم فيه استضافة حزمتك على الخادم. لذلك ، على سبيل المثال ، إذا كان publicPath الخاص بك هو /public/assets ، فستظهر الحزمة تحت /public/assets/bundle.js على الخادم.
devServer: { contentBase: PROD ? './build' : './app' }هذا يخبر الخادم بالمجلد في مشروعك لاستخدامه كدليل جذر للخادم.
إذا شعرت بالحيرة بشأن كيفية تعيين Webpack للحزمة التي تم إنشاؤها في مشروعك للحزمة الموجودة على الخادم ، فتذكر ببساطة ما يلي:
-
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 أن أداة التحميل المحددة تنطبق إذا كان الملف يطابق نمط regex المقدم ، في هذه الحالة /\.scss$/ . تتوافق خاصية loader مع الإجراء الذي يقوم به المحمل. نحن هنا نربط معًا لوادر style و css و resolve-url و sass ، والتي يتم تنفيذها بترتيب عكسي.
يجب أن أعترف أنني لم أجد loader3!loader2!loader1 أنيق للغاية. بعد كل شيء ، متى يجب عليك قراءة أي شيء في برنامج من اليمين إلى اليسار؟ على الرغم من ذلك ، تعتبر أدوات التحميل ميزة قوية جدًا في حزمة الويب. في الواقع ، يسمح لنا المُحمل الذي ذكرته للتو باستيراد ملفات SASS مباشرة إلى JavaScript! على سبيل المثال ، يمكننا استيراد أوراق أنماط البائعين والعالمية في ملف نقطة الدخول الخاص بنا:
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' لاستيراد ورقة الأنماط المرتبطة بالمكون. هذا ينطبق أيضًا على جميع مكوناتنا الأخرى.
في رأيي ، يمكن اعتبار هذا تقريبًا تغييرًا ثوريًا في عالم تطوير JavaScript. لا داعي للقلق بشأن حزم CSS أو تصغيرها أو خرائط مصادرها نظرًا لأن أداة التحميل لدينا تتولى كل هذا من أجلنا. حتى إعادة تحميل الوحدة النمطية تعمل مع ملفات CSS الخاصة بنا. ومن ثم ، فإن القدرة على التعامل مع واردات JS و CSS في نفس الملف تجعل التطوير أبسط من الناحية المفاهيمية: مزيد من التناسق ، وتبديل أقل للسياق ، وأسهل في التفكير.
لإعطاء ملخص موجز عن كيفية عمل هذه الميزة: يقوم Webpack بتضمين CSS في حزمة JS الخاصة بنا. في الواقع ، يمكن لـ 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]'}يرشد محمل URL Webpack إلى تضمين صورنا وخطوطنا كعناوين url للبيانات إذا كانت أقل من 100 كيلوبايت ، وإلا فخدمها كملفات منفصلة. بالطبع ، يمكننا أيضًا تكوين حجم القطع إلى قيمة مختلفة مثل 10 كيلوبايت.
وهذا هو تكوين Webpack باختصار. سأعترف أن هناك قدرًا لا بأس به من الإعداد ، لكن فوائد استخدامه هي ببساطة استثنائية. على الرغم من أن Browserify يحتوي على مكونات إضافية وتحويلات ، إلا أنه لا يمكن مقارنتها ببساطة ببرامج تحميل Webpack من حيث الوظائف المضافة.
Webpack + NPM Scripts Setup (إعداد البرامج النصية لـ Webpack + NPM)
في هذا الإعداد ، نستخدم البرامج النصية npm مباشرةً بدلاً من الاعتماد على ملف 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 ، على التوالي.
هذا بالتأكيد أكثر إحكاما من ملف gulp الخاص بنا ، نظرًا لأننا قمنا بقص 99 إلى 150 سطرًا من التعليمات البرمجية وصولاً إلى 19 نصوصًا NPM ، أو 12 إذا استبعدنا نصوص الإنتاج (معظمها يعكس فقط البرامج النصية للتطوير مع بيئة العقدة التي تم ضبطها على الإنتاج ). العيب هو أن هذه الأوامر غامضة إلى حد ما مقارنة بنظرائنا في مهام Gulp ، وليست معبرة تمامًا. على سبيل المثال ، لا توجد طريقة (على الأقل أعرفها) لجعل برنامج نصي npm واحد يقوم بتشغيل أوامر معينة في سلسلة وأخرى على التوازي. إما أن يكون أحدهما أو الآخر.
ومع ذلك ، هناك ميزة كبيرة لهذا النهج. باستخدام مكتبات NPM مثل mocha مباشرة من سطر الأوامر ، لا تحتاج إلى تثبيت غلاف Gulp مكافئ لكل منها (في هذه الحالة ، gulp-mocha ).
بدلاً من تثبيت NPM
- بلع ايسلينت
- بلع موكا
- بلع نوديمون
- إلخ
نقوم بتثبيت الحزم التالية:
- eslint
- موكا
- nodemon
- إلخ
نقلاً عن منشور Cory House ، لماذا تركت Gulp و Grunt لبرامج NPM :
كنت من أشد المعجبين بـ Gulp. لكن في مشروعي الأخير ، انتهى بي الأمر بمئات السطور في ملف gulp الخاص بي وحوالي عشرة من مكونات Gulp الإضافية. كنت أعاني من أجل دمج Webpack و Browsersync وإعادة التحميل السريع و Mocha وغير ذلك الكثير باستخدام Gulp. لماذا ا؟ حسنًا ، بعض المكونات الإضافية لا تحتوي على وثائق كافية لحالة الاستخدام الخاصة بي. كشفت بعض المكونات الإضافية عن جزء من واجهة برمجة التطبيقات التي أحتاجها فقط. كان لدى أحدهم خطأ غريب حيث كان يشاهد فقط عددًا صغيرًا من الملفات. ألوان أخرى مجردة عند الإخراج إلى سطر الأوامر.
لقد حدد ثلاث قضايا أساسية مع Gulp:
- الاعتماد على مؤلفي البرنامج المساعد
- محبط للتصحيح
- وثائق مفككة
أنا أميل إلى الاتفاق مع كل هؤلاء.
1. الاعتماد على مؤلفي البرنامج المساعد
عندما يتم تحديث مكتبة مثل eslint ، تحتاج مكتبة gulp-eslint المرتبطة إلى تحديث مطابق. إذا فقد مدير المكتبة الاهتمام ، فإن نسخة المكتبة من المكتبة تصبح غير متزامنة مع النسخة الأصلية. الشيء نفسه ينطبق عندما يتم إنشاء مكتبة جديدة. إذا قام شخص ما بإنشاء مكتبة xyz في الظهور ، فأنت بحاجة فجأة إلى مكتبة gulp-xyz المقابلة لاستخدامها في مهام gulp الخاصة بك.
بمعنى ما ، هذا النهج لا مقياس. من الناحية المثالية ، نريد نهجًا مثل Gulp يمكنه استخدام المكتبات الأصلية.
2. محبط للتصحيح
على الرغم من أن المكتبات مثل gulp-plumber تساعد في التخفيف من هذه المشكلة إلى حد كبير ، إلا أنه من الصحيح مع ذلك أن الإبلاغ عن الأخطاء في gulp ليس مفيدًا للغاية. إذا ألقى أنبوب واحد استثناءً غير معالج ، فستحصل على تتبع مكدس لمشكلة تبدو غير مرتبطة تمامًا بما يسبب المشكلة في كود المصدر الخاص بك. هذا يمكن أن يجعل تصحيح الأخطاء كابوسًا في بعض الحالات. لا يمكن لأي قدر من البحث على Google أو Stack Overflow مساعدتك حقًا إذا كان الخطأ مشفرًا أو مضللًا بدرجة كافية.
3. التوثيق المفكك
في كثير من الأحيان أجد أن مكتبات gulp الصغيرة تميل إلى التوثيق المحدود للغاية. أظن أن السبب في ذلك هو أن المؤلف عادة ما يصنع المكتبة لاستخدامه الخاص في المقام الأول. بالإضافة إلى ذلك ، من الشائع أن تضطر إلى النظر في الوثائق لكل من مكون Gulp الإضافي والمكتبة الأصلية نفسها ، مما يعني الكثير من تبديل السياق ومضاعفة القراءة التي يجب القيام بها.
خاتمة
يبدو واضحًا جدًا بالنسبة لي أن Webpack أفضل من Browserify وأن البرامج النصية NPM مفضلة على Gulp ، على الرغم من أن كل خيار له فوائده وعيوبه. من المؤكد أن Gulp أكثر تعبيرًا وملاءمة للاستخدام من نصوص NPM ، لكنك تدفع الثمن في كل التجريد الإضافي.
قد لا تكون كل مجموعة مثالية لتطبيقك ، ولكن إذا كنت ترغب في تجنب عدد هائل من تبعيات التطوير وتجربة تصحيح أخطاء محبطة ، فإن Webpack مع البرامج النصية NPM هي السبيل للذهاب. أتمنى أن تجد هذه المقالة مفيدة في اختيار الأدوات المناسبة لمشروعك القادم.
- الحفاظ على التحكم: دليل Webpack و React ، Pt. 1
- Gulp Under the Hood: إنشاء أداة لأتمتة المهام قائمة على التدفق
