טוענים את ה-Guntfile שלך

איך להפיק את המקסימום מתצורת ה-build

מבוא

אם עולם הגראנט לא חדש לכם, מקום אידיאלי להתחיל בו הוא המאמר המעולה של כריס קוייר, "Grunt for אנשים שחושבים דברים כמו GRunt הם מוזרים וכבדים". אחרי ההקדמה של כריס, תגדירו פרויקט משלכם ותטעמו טעימה מהצעות ה-Grunt.

במאמר הזה לא נתמקד במה שיישומי הפלאגין רבים של גראנט עושים לקוד הפרויקט שלכם, אלא בתהליך ה-build של גראנט עצמו. נציג לכם רעיונות מעשיים בנושאים הבאים:

  • איך לשמור על קובץ ה-Gruntfile שלך בצורה מסודרת ומסודרת,
  • איך משפרים משמעותית את זמן ה-build?
  • ואיך לקבל התראות בכל פעם ש-build מתרחש.

הגיע הזמן לכתב ויתור קצר: גראנט הוא רק אחד מהכלים הרבים שאפשר להשתמש בהם כדי לבצע את המשימה. אם Gulp הוא יותר סגנון שלך, מעולה! אם אחרי שבדקתם את האפשרויות עדיין תרצו לבנות שרשרת כלים משלכם, זה בסדר! בחרנו להתמקד ב-Grunt במאמר הזה בגלל הסביבה העסקית החזקה ובסיס המשתמשים הוותיק שלו.

ארגון קובץ ה-Gruntfile

בין אם כוללים הרבה יישומי פלאגין של גראנט או צריך לכתוב הרבה משימות ידניות בקובץ ה-Gruntfile, הוא עלול להפוך במהירות לבלתי מסורבל וקשה לתחזוקה. למרבה המזל, יש לא מעט יישומי פלאגין שמתמקדים בדיוק בבעיה הזו: שמירה על קובץ ה-Gruntfile בצורה מסודרת ומסודרת שוב.

קובץ ה-Gruntfile, לפני האופטימיזציה

כך נראה קובץ ה-Gruntfile שלנו לפני שביצענו אופטימיזציה:

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },

      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-imagemin');

  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);

};

אם עכשיו אומרים "היי! ציפיתי להרבה יותר גרוע! זה בעצם דבר שאפשר לתחזק אותה!", סביר להניח שאתם צודקים. כדי לפשט את העניינים, כללנו רק שלושה יישומי פלאגין ללא הרבה התאמה אישית. השימוש ב-Gruntfile גרסת ייצור של פרויקט בגודל בינוני יחייב גלילה מתמשכת במאמר הזה. אז בואו נראה מה אפשר לעשות!

טעינה אוטומטית של יישומי הפלאגין של גראנט

כשמוסיפים פלאגין חדש של Graunt, שבו רוצים להשתמש בפרויקט, צריך להוסיף את שניהם לקובץ package.json כתלוי npm ואז לטעון אותו בקובץ Grauntfile. הפלאגין grunt-contrib-concat עשוי להיראות כך:

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

אם תסירו עכשיו את הפלאגין דרך npm ותעדכנו את package.json, אבל תשכחו לעדכן את ה-Gruntfile, ה-build לא יפעל. זה המקום שבו הפלאגין המגניב load-grunt-tasks יכול לעזור.

בעבר, היינו צריכים לטעון את יישומי הפלאגין של גראנט באופן ידני, למשל:

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-imagemin');

עם Charge-grunts, תוכלו לכווץ אותם לשורה הבאה:

require('load-grunt-tasks')(grunt);

אחרי דרישת הפלאגין, הוא ינתח את הקובץ package.json, יקבע אילו יחסי התלות הם יישומי פלאגין של גראנט ויטען את כולם באופן אוטומטי.

פיצול ההגדרות האישיות של גראנט לקבצים שונים

הפקודה load-grunt-tasks כווצה את קובץ ה-Gruntfile בקוד ובמורכבות שלו, אבל אם מגדירים אפליקציה גדולה, הוא עדיין יהפוך לקובץ גדול מאוד. כאן נכנס לתמונה load-grunt-config. הכלי load-grunt-config מאפשר לחלק את ההגדרות של Grauntfile לפי משימה. בנוסף, הוא כולל גם את load-grunt-tasks ואת הפונקציונליות שלו!

עם זאת, חשוב לזכור שפיצול קובץ ה-Gruntfile לא תמיד מתאים לכל מצב. אם יש לך הרבה הגדרות משותפות בין המשימות (כלומר, אתה משתמש הרבה בתבניות גראנט), עליך להיות קצת זהיר.

עם load-grunt-config, ה-Gruntfile.js ייראה כך:

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

כן, זהו באמת, כל הקובץ! איפה נמצאות ההגדרות האישיות שלנו?

יוצרים תיקייה בשם grunt/ בספרייה של Grandfile. כברירת מחדל, הפלאגין כולל קבצים בתיקייה הזו שתואמים לשם המשימה שבה רוצים להשתמש. מבנה הספריות שלנו אמור להיראות כך:

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

עכשיו נוסיף את הגדרות המשימות של כל אחת מהמשימות ישירות לקבצים המתאימים (תראו שהפעולות האלה הן בעיקר העתקה והדבקה מה-Gruntfile המקורי במבנה חדש):

grunt/concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt/uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt/imagemin.js

module.exports = {
  options: {
    cache: false
  },

  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

אם בלוקים של הגדרות JavaScript לא באמת מתאימים לכם, load-grunt-tasks מאפשר לכם להשתמש במקום זאת בתחביר של YAML או ContactScript. בואו נכתוב את הקובץ האחרון הנדרש ב-YAML – קובץ aliases. זהו קובץ מיוחד שרושם כתובות אימייל חלופיות של משימות, כפי שהיה צריך לעשות קודם כחלק מה-Gruntfile באמצעות הפונקציה registerTask. הנה שלנו:

grunt/aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

זהו, סיימנו! מריצים את הפקודה הבאה בטרמינל:

$ grunt

אם הכול עבד טוב, המשימה תבוצע עכשיו 'ברירת מחדל' ותריץ את כל הפעולות לפי הסדר. עכשיו, אחרי שהסרנו את קובץ ה-Gruntfile הראשי משלוש שורות קוד שלעולם לא צריך לגעת בו, והצלחנו להציג כל תצורה של המשימה באופן חיצוני. אבל אחלה, עדיין די איטי לבנות את הכל. בואו נראה איך נוכל לשפר את זה.

קיצור זמן ה-build

למרות שביצועי זמן הריצה וזמן הטעינה של אפליקציית האינטרנט שלכם הם קריטיים הרבה יותר לעסק מאשר הזמן שנדרש להפעלת ה-build, פיתוח איטי הוא עדיין בעייתי. יהיה קשה יותר להפעיל גרסאות build אוטומטיות באמצעות יישומי פלאגין כמו grunt-contrib-watch או לאחר ביצוע Git Pro מספיק מהר. בנוסף, יהיה קנס על ההפעלה בפועל של ה-build. ככל שזמן ה-build מהיר יותר, תהליך העבודה גמיש יותר. אם הרצת ה-build לסביבת הייצור נמשכת יותר מ-10 דקות, תפעילו את ה-build רק כשצריך, ותישארו בבית קפה בזמן שהוא פועל. זה נגמר תפוקה. יש לנו מה להאיץ.

רק גרסאות build של קבצים שהשתנו: grun-new יותר

לאחר בנייה ראשונית של האתר, סביר להניח שנגיעתם רק בכמה קבצים בפרויקט כשתתקלו שוב בבנייה. נניח שבדוגמה שלנו שיניתם תמונה בספרייה src/img/ – הגיוני להפעיל imagemin כדי לבצע אופטימיזציה מחדש של תמונות, אבל רק עבור התמונה הבודדת הזו – וכמובן, הפעלה מחדש של concat ו-uglify מבזבזת מחזורי מעבד יקרים.

כמובן שתמיד אפשר להריץ $ grunt imagemin מהטרמינל במקום $ grunt כדי לבצע באופן סלקטיבי רק משימה שצריך לבצע, אבל יש דרך חכמה יותר. הוא נקרא grunt-newer.

למשתמש החדש גראנט יש מטמון מקומי שבו הוא מאחסן מידע על הקבצים שהשתנו בפועל ומבצע את המשימות רק בקבצים שהשתנו, למעשה. בואו נראה כיצד להפעיל אותה.

זוכרת את הקובץ aliases.yaml שלנו? אפשר לשנות זאת כך:

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

עד כאן:

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

פשוט מציבים מראש את הטקסט "newer: " לכל אחת מהמשימות. מעבירים את קובצי המקור והיעד דרך הפלאגין החדש 'הגדול יותר'. לאחר מכן, המשימה תקבע אילו קבצים, אם בכלל, תריץ.

הרצה של מספר משימות במקביל: grunt-concurrent

grunt-concurrent הוא פלאגין שהופך להיות שימושי במיוחד כשיש הרבה משימות שלא תלויות אחת בשנייה וצורכות הרבה זמן. היא משתמשת במספר המעבדים (CPU) במכשיר ומבצעת מספר משימות במקביל.

והכי חשוב, התצורה שלו פשוטה מאוד. בהנחה שאתם משתמשים ב-load-grunt-config, יוצרים את הקובץ החדש הבא:

grunt/concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

פשוט מגדירים מסלולי הפעלה מקבילים בשמות 'first' ו-'second'. צריך קודם להריץ את משימת concat, ובינתיים אין לנו מה להריץ. במסלול השני שלנו, אנחנו שמים גם את uglify וגם את imagemin, כי שני הסוגים בלתי תלויים זה בזה, ושניהם צורכים לא מעט זמן.

זה לבד לא עושה שום דבר. אנחנו צריכים לשנות את ברירת המחדל של המשימה החלופית שלנו כדי להפנות למשימות שפועלות באותו זמן במקום למשימות הישירות. זה התוכן החדש של grunt/aliases.yaml:

default:
  - 'concurrent:first'
  - 'concurrent:second'

אם עכשיו תריצו מחדש את גרסת ה-grunt, הפלאגין המקביל בו-זמנית יריץ קודם את משימת ה-concat ואז יפיק שני שרשורים בשתי ליבות מעבד (CPU) שונות, כדי להריץ גם את imagemin וגם את uglify במקביל. יש!

כדאי לכתוב לנו רק המלצה: רוב הסיכויים שבדוגמה הבסיסית שלנו, שימוש בו-זמנית לא יקפיץ את גרסת ה-build הרבה יותר מהר. הסיבה לכך היא התקורה שנוצרה על ידי הבאת מופעים שונים של גראנט בשרשורים שונים: במקרה שלי, לפחות 300 אלפיות השנייה.

כמה זמן נמשך התהליך?

עכשיו, אחרי שאנחנו מבצעים אופטימיזציה של כל אחת מהמשימות שלנו, חשוב מאוד להבין כמה זמן נדרש כדי לבצע כל משימה בנפרד. למרבה המזל, יש גם פלאגין שמאפשר זאת: time-grunt.

time-grunt הוא לא פלאגין grunt קלאסי שטוענים כמשימה של npm, אלא פלאגין שכלל ישירות, בדומה ל-load-grunt-config. אנחנו נוסיף דרישה ל-Guntfile של Time-grunt, בדיוק כמו שעשינו עם load-grunt-config. עכשיו קובץ ה-Gruntfile שלנו אמור להיראות כך:

module.exports = function(grunt) {

  // measures the time each task takes
  require('time-grunt')(grunt);

  // load grunt config
  require('load-grunt-config')(grunt);

};

לצערי זה מאכזב, אבל זהו – נסו להריץ שוב את Graunt מה-Terminal שלכם ועבור כל משימה (ובנוסף לכל ה-build), אמורה להופיע חלונית מידע בפורמט יפה בזמן הביצוע:

זמן לגראנט

התראות מערכת אוטומטיות

עכשיו, אחרי שיש לכם גרסת build שעברו אופטימיזציה מלאה של Graunt, שפועלת במהירות וסיפקתם לבנות אותה באופן אוטומטי בדרך כלשהי (למשל, על ידי צפייה בקבצים באמצעות grunt-contrib-watch או אחרי שמירות), נכון שזה יהיה נהדר אם המערכת שלכם תוכל להודיע לכם מתי ה-build החדש מוכן לשימוש או כשקרתה משהו רע? רוצים להכיר את grunt-notify?

כברירת מחדל, grunt-notify מספק התראות אוטומטיות על כל השגיאות והאזהרות של גראנט באמצעות מערכת ההתראות הזמינה במערכת ההפעלה שלכם: Growl for OS X או Windows, מרכז ההתראות של Mountain Lion's ו-Mavericks ו-Notify-send. למרבה הפלא, כל מה שצריך כדי לקבל את הפונקציונליות הזו הוא להתקין את הפלאגין מ-npm ולטעון אותו בקובץ Grauntfile שלך (חשוב לזכור, אם אתם משתמשים ב-grunt-load-config למעלה, השלב הזה הוא אוטומטי!).

כך זה ייראה, בהתאם למערכת ההפעלה:

התראה

בנוסף לשגיאות ולאזהרות, כדאי להגדיר אותו כך שיפעל אחרי שהמשימה האחרונה הסתיימה. בהנחה שאתם משתמשים ב-grunt-load-config כדי לפצל משימות בין קבצים, זה הקובץ שדרוש לנו:

grunt/notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

ברמה הראשונה של אובייקט ה-config, המפתח צריך להיות תואם לשם המשימה שאליה אנחנו רוצים לחבר אותו. בדוגמה הזו ההודעה תופיע מיד אחרי ביצוע המשימה imagemin, שהיא המשימה האחרונה בשרשרת ה-build שלנו.

כמעט סיימנו

אם עקבתם אחרי הקוד, אתם עכשיו הבעלים הגאים של תהליך build מסודר ומאורגן, מהיר במיוחד בזכות טעינה מקבילה ועיבוד סלקטיבי ומודיע לכם בכל פעם שמשהו משתבש.

אם גיליתם עוד אבן חן שמשפרת את Garunt ואת יישומי הפלאגין שלה, נשמח לשמוע על כך! עד אז, תודה מראש!

עדכון (14/2/2014): לחצו כאן כדי לקבל עותק של הפרויקט המלא והפעיל לדוגמה Graunt.