ארזתם לבד את פרויקט הווב שלכם? Webpack יכול לעזור לכם

רקע

Webpack הוא דבר שימושי ונפלא, אך כדי להסביר מהם יתרונותיו בואו ונחזור כמה שנים אחורה לתקופה בה רוב העיבוד נעשה בצד-השרת ואילו אחריות צד-הלקוח הסתכמה בדרך כלל באנימציות נחמדות וכמה קריאות AJAX.
ובכן, בתקופה ההיא אתר היה טוען קובץ Javascript יחיד בו רוכזה כל פונקציונליות ה-Front-end. קובץ זה היה נטען לדף באמצעות תגית script ומושגים כמו Lazy Loading ו-Module System נראו רלוונטיות לצד-השרת בלבד.

ניהול קוד בצד-לקוח

בשנים האחרונות, עם המעבר לעיבוד בצד-הלקוח ופריחתן של ספריות התומכות בכך (החל מ-jQuery ועד React), כמות קוד ה-Javascript בדפים עלתה בצורה ניכרת.
הצורך בניהול כמות גדולה של קוד הביא לפיתוח של שני סוגי פתרונות אשר נמצאים בשימוש כיום כמעט בכל פרויקט צד-לקוח:

  1. כלי הרצת משימות (לדוגמה: Grunt, Gulp).
  2. מערכות לניהול רכיבים (Require.js, curl.js).

פתרונות אלו אמנם מקלים על ניהול הקוד והופכים את התחזוקה ל״נסבלת״, אך יש בהם מספר חסרונות בולטים וביניהם:

  1. ״שיעבוד״ לצורת כתיבה מסוימת: קיומם של עשרות קובצי קונפיגורציה עלול ליצור תלות מוגזמת ב-Grunt. בדומה לכך, שימוש ב-Syntax של Require יוצר תלות שתקשה על מעבר למערכת אחרת (לדוגמה ES6 Modules). במקרה של Grunt זהו הרע במיעוטו מפני שקיימת ״מחיצה״ בין תהליך הבנייה לבין זמן-הריצה, אך דמיינו עשרות, אם לא מאות רכיבים, אשר כוללים בתוכם Syntax ״זר״ של Require.js.
  2. אותה ״מחיצה״ אשר הזכרתי בסעיף הקודם, בין תהליך הבנייה ב-Grunt לבין זמן-הריצה טומנת בחובה גם תופעה שלילית בדמות היווצרות ישות נוספת בפרויקט, היא ״תהליך הבנייה״. כולנו מכירים בכך שעל-מנת להביא שקוד יהיה תקין, יציב ומתוחזק היטב עלינו להשקיע מאמצים רבים. קיומו של ״תהליך בניה״ ההכרחי להרצת הקוד מקשה על תפעול המערכת ומצריך תחזוקה שוטפת שלא תמיד מצדיק את ה-ROI: יצירת משימות, אופטימיזציה, עדכון פלאגינים ועוד…

Webpack מציע פתרונות לבעיות אלה ואחרות באמצעות:

  1. חלוקת קוד קיים לנתחים, ומתן יכולת לטעון נתחים בנפרד ובצורה אסינכרונית.
  2. טעינת קבצים סטאטיים מכל סוג שהוא: ניתן לטעון תמונות, קובצי CSS ואפילו Web Fonts לתוך הדף. במקרה של התמונות, אפשר אפילו לקבוע גודל מסויים אשר תמונות קטנות ממנו יתווספו לדף בתור תגית img מסוג base64 ואילו תמונות גדולות תקבלנה כתובת ב-src.
  3. המרת קבצים לתצורות שונות באמצעות Loaders. למשל: ניתן להמיר CoffeeScript ל-Javascript, או CSS לתגית style.
  4. ביצוע פעולות על תוכן הקובץ באמצעות בפלאגינים. למשל: ניתן לתרגם מילים באמצעות הפלאגין I18nPlugin.

את כל זה ניתן לגרום ל-Webpack לבצע באמצעות כתיבה ב-Syntax המועדף עליכם: Common.js, Require.js או ES6 Modules.

כיצד Webpack עובד?

התקנה

$ npm install webpack -g

פעולתו של Webpack מוגדרת באמצעות קובץ קונפיגורציה בשם webpack.config.js:

module.exports = {
  entry: ['./js/Calc.js', './js/app.js'],
  output: {
    filename: './build/bundle.js'
  },
  module: {
    loaders: [
      { test: /\.js$/, loader: 'babel-loader' }
    ]
  }
}

נתקין Webpack Plugin של Babel שיבצע Transpile לקוד ה-ES6:

$ npm install babel-loader

Calc.js

ניצור רכיב מחשבון:

module.exports= {
    pow: (num) => (num * num)
}

app.js

נטען את רכיב המחשבון (באמצעות CommonJS Syntax) ונשתמש בו:

let Calc = require('./Calc.js');
console.log( Calc.pow(4) );

לאחר הרצת הפקודה הבאה, Webpack ייצר את הקובץ bundle.js:

$ webpack

bundle.js

קובץ זה כולל 2 חלקים, כאשר החלק הראשון הוא חלק מהתשתית של Webpack והאחר הוא הקוד שאנחנו כותבים:

חלק ראשון (Webpack Bootstrap):
/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};

/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {

/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId])
/******/ 			return installedModules[moduleId].exports;

/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			exports: {},
/******/ 			id: moduleId,
/******/ 			loaded: false
/******/ 		};

/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

/******/ 		// Flag the module as loaded
/******/ 		module.loaded = true;

/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}


/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;

/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;

/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";

/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(0);
/******/ })
/************************************************************************/

חלק זה הוא Self-Invoked Function הכוללת את הרכיבים הבאים:

  1. installedModules – מטמון (Cache) של רכיבים. מאפשר שימוש-חוזר ועל-ידי כך משפר Performance (כמו כל מנגנון Cahce אחר…).
  2. __webpack_require__ – זוהי פונקציית Closure המקבלת כקלט מזהה-רכיב, בודקת האם הרכיב נמצא ב-Cache ואם לא – מכניסה אותו אליו.
  3. module.loaded – סימון טעינת הרכיב לאחר ההפעלה שלו (באמצעות call).
חלק שני: טעינת הרכיבים
/* 0 */
/***/ function(module, exports, __webpack_require__) {
	__webpack_require__(1);
	module.exports = __webpack_require__(2);
/***/ },
/* 1 */
/***/ function(module, exports) {
	"use strict";
	module.exports = {
	    pow: function pow(num) {
	        return num * num;
	    }
	};
/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {
	'use strict';
	var Calc = __webpack_require__(1);
	console.log(Calc.pow(4));
/***/ }
/******/ ]);

חלק זה גם הוא Self-Invoked, אלא שהפעם מדובר במערך של פונקציות שנקראות אחת-אחר-השנייה:

  1. הפונקציה הראשונה (מסומנת כ-/* 0 */) נוצרת ע״י Webpack ותפקידה הוא להכין לפעולה את הפונקציות שכללנו בשדה entry בקובץ הקונפיגורציה שלנו (מסומנות כ-1 ו-2).
  2. הפונקציה השנייה (מסומנת כ-/* 1 */) היא רכיב המחשבון (Calc.js) אשר הפונקציונליות שלו מועברת דרך השדה module.exports. כל הפונקציות בקובץ מקבלות את module כפרמטר.
    כפי שאפשר לראות, הקוד ברכיב זה כתוב ב-ES5 הודות לפלאגין של Babel.
  3. הפונקציה האחרונה (מסומנת כ-/* 2 */) היא האפליקציה שלנו (app.js). המיוחד בה הוא הקריאה הבאה:

    var Calc = __webpack_require__(1);
    

    זהו שימוש חוזר בפונקציה __webpack_require__, אשר הפעם מחזירה את רכיב המחשבון. להזכירכם, לפני הרצת Webpack הקוד היה:

    let Calc = require('./Calc.js');
    

דוגמה נוספת: שימוש חוזר ברכיבים

נניח שישנם 2 רכיבים (a ו-b) אשר משתמשים ברכיב שלישי – style.css:

a.js

require("./style.css");
require("./styleA.css");

b.js

require("./style.css");
require("./styleB.css");

במקרה כזה, לא נרצה לארוז פעמיים את הרכיב.
פלאגין של Webpack בשם CommonsChunkPlugin, מאפשר לארוז את הרכיב פעם אחת ולהשתמש בו פעמיים:

webpack.config.js

var path = require("path");

module.exports = {
  entry: {
    A: "./a",
    B: "./b"
  },
  output: {
   path: path.join(__dirname, "js"),
   filename: "[name].js"
  },
  module: {
    loaders: [
      {
        test: /\.css$/,
        loader: 'style-loader!css-loader'
      }
    ]
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
            name: "commons",
            filename: "commons.js",
            chunks: ["A", "B"]
        })
 ]
}

לאחר הרצת Webpack, יווצר הקובץ common.js והוא יכלול את הרכיב המשותף, אשר יורכב מ-a ו-b.

דוגמה לפני-אחרונה: Lazy Loading

מאחר ואנחנו משתמשים בתחביר של common.js, באפשרותנו לטעון רכיבים לפי דרישה:

if (!location.hash || location.hash.length === 1) {
    require.ensure([], function () {
      var Home = require('./Home.js');
      console.log(Home.inspect());
    });
} else if (location.hash === '#admin') {
    require.ensure([], function () {
      var Admin = require('./Admin.js');
      console.log(Admin.inspect());
    });
}

בדוגמה הזו Webpack יארוז עבורנו את שני הרכיבים – Home ו-Admin, אך יטען אותם בהתאם לפרמטרים המועברים ב-URL.

דוגמה אחרונה: טעינת קבצים סטאטיים

באמצעות Webpack ניתן לטעון קבצים מסוגים שונים: jpeg, css, woff…
בנוסף, ניתן להגדיר גודל קובץ מקסימלי לטעינה כחלק מהדף בתור base64. קבצים גדולים יותר ייטענו מקובץ חיצוני.
לדוגמה, אנו יכולים להגביל את גדלי התמונות בדפים שלנו ל-8 קילובייט. כל תמונה אשר תחרוג מגודל זה – תיטען מהמיקום הפיזי שלה:

webpack.config.js

module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}
    ]
  }
}

לסיכום: בתקופה האחרונה עולם הפיתוח בסביבת פרונט-אנד עובר לגישה אשר במרכזה הרכיבים (Components). גישה זו היא הרעיון המרכזי בשתי הטכנולוגיות המובילות את עולם הפרונט בימים אלו, Angular ו-React. נראה כי פתרונות מסוגם של Webpack ודומיו (Browserify לדוגמה) אשר תומכים ומסייעים לתהליך הפיתוח מונחה-הרכיבים יתפסו מקום משמעותי בזמן הקרוב.