เพิ่มประสิทธิภาพไฟล์ Gruntfile

วิธีใช้การกำหนดค่าบิลด์ให้เกิดประโยชน์สูงสุด

เกริ่นนำ

ถ้าโลกของ Grunt นั้นใหม่สำหรับคุณ บทความที่ยอดเยี่ยมของ Chris Coyier คือ "Grunt for People Who Think Things Like Grunt are Weird and Hard" หลังจากแนะนำ Chris แล้ว คุณจะสร้างโปรเจ็กต์ Grunt ขึ้นมาและได้ลองสัมผัสพลังของ Grunt บางส่วน

ในบทความนี้ เราจะไม่เน้นถึงการทำงานของปลั๊กอิน Grunt จำนวนมากกับโค้ดโปรเจ็กต์จริงของคุณ แต่อยู่ที่ขั้นตอนการสร้าง Grunt เราจะให้แนวคิดที่นำไปปฏิบัติได้จริงเกี่ยวกับเรื่องต่อไปนี้

  • วิธีดูแล Gruntfile ให้เป็นระเบียบเรียบร้อย
  • วิธีปรับปรุงเวลาสร้างอย่างมาก
  • และวิธีรับการแจ้งเตือนเมื่อมีการสร้างบิลด์

ได้เวลาจำกัดความรับผิดชอบสั้นๆ แล้ว: Grunt เป็นเพียงเครื่องมือหนึ่งในหลายๆ เครื่องมือที่คุณสามารถใช้เพื่อทำงานให้สำเร็จ ถ้า Gulp คือสไตล์ของคุณ ก็เยี่ยมเลย หากคุณได้สำรวจทางเลือกต่างๆ ไปแล้วและยังต้องการสร้าง Toolchain ของตัวเองก็ทำได้เช่นกัน เราเลือกให้ความสำคัญกับ Grunt สำหรับบทความนี้เนื่องจากมีระบบนิเวศที่แข็งแกร่งและฐานผู้ใช้ที่ยั่งยืน

การจัดระเบียบ Gruntfile

ไม่ว่าคุณจะใส่ปลั๊กอิน Grunt จำนวนมากหรือต้องเขียนงานที่ต้องใช้แรงหลายอย่างไว้ใน 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']);

};

หากตอนนี้มีการพูดว่า "Ok! เราคาดว่าอาการแย่ลงมาก! ซึ่งสามารถบำรุงรักษาได้จริงๆ" คุณอาจตอบถูก เพื่อความสะดวก เราได้รวมปลั๊กอินเพียง 3 ปลั๊กอินโดยไม่มีการปรับแต่งใดๆ มากนัก การใช้ Gruntfile จริงในการสร้างโปรเจ็กต์ที่มีขนาดปานกลางจะต้องเลื่อนได้ไม่รู้จบในบทความนี้ มาดูกันว่าเราทำอะไรได้บ้าง

โหลดปลั๊กอิน Grunt โดยอัตโนมัติ

เมื่อเพิ่มปลั๊กอิน Grunt ใหม่ที่ต้องการใช้ในโปรเจ็กต์ คุณจะต้องเพิ่มปลั๊กอินทั้งสองไปยังไฟล์ package.json เป็นทรัพยากร Dependency แบบ NPM แล้วจึงโหลดลงใน Gruntfile สำหรับปลั๊กอิน “grunt-contrib-concat” อาจมีลักษณะดังนี้

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

หากคุณถอนการติดตั้งปลั๊กอินผ่าน npm และอัปเดต package.json แต่ลืมอัปเดต Gruntfile บิลด์ของคุณจะเสียหาย และนี่คือจุดที่ปลั๊กอินสุดเก๋ load-grunt-tasks จะเข้ามาช่วยได้

ก่อนหน้านี้เราต้องโหลดปลั๊กอิน Grunt ด้วยตนเอง ดังนี้

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

ด้วยงานหนัก

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

หลังจากที่ต้องใช้ปลั๊กอิน ระบบจะวิเคราะห์ไฟล์ package.json ของคุณเพื่อพิจารณาว่าทรัพยากร Dependency ใดเป็นปลั๊กอิน Grunt จากนั้นโหลดปลั๊กอินทั้งหมดโดยอัตโนมัติ

แยกการกำหนดค่า Grunt ออกเป็นไฟล์ต่างๆ

load-grunt-tasks จะลดขนาด Gruntfile ลงในโค้ดและความซับซ้อนเล็กน้อย แต่เมื่อคุณกำหนดค่าแอปพลิเคชันขนาดใหญ่ ไฟล์ดังกล่าวจะยังคงกลายเป็นไฟล์ที่มีขนาดใหญ่มาก ส่วน load-grunt-config จะเข้ามามีบทบาท load-grunt-config จะช่วยให้คุณแบ่งการกำหนดค่า Gruntfile ตามงานได้ ยิ่งไปกว่านั้น ยังสรุป load-grunt-tasks และฟังก์ชันการทำงานด้วย

ข้อมูลสำคัญ: การแยก Gruntfile อาจไม่ได้ผลในบางสถานการณ์ หากคุณมีการกำหนดค่าที่ใช้ร่วมกันจำนวนมากระหว่างงานต่างๆ (เช่น ใช้เทมเพลต Grunt จำนวนมาก) คุณควรระมัดระวังเล็กน้อย

เมื่อใช้ load-grunt-config Gruntfile.js จะมีลักษณะดังนี้

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

ใช่เลย เล่นมาทั้งไฟล์เลย การกำหนดค่างานของเราย้ายไปอยู่ที่ส่วนใดแล้ว

สร้างโฟลเดอร์ชื่อ grunt/ ในไดเรกทอรีของ Gruntfile โดยค่าเริ่มต้น ปลั๊กอินจะมีไฟล์ในโฟลเดอร์นั้นซึ่งตรงกับชื่องานที่คุณต้องการใช้ โครงสร้างไดเรกทอรีของเราควรมีลักษณะดังนี้

- 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 หรือ CoffeeScript แทนได้ เรามาเขียนไฟล์สุดท้ายที่จำเป็นใน YAML ซึ่งเป็นไฟล์ “aliases” กัน ไฟล์นี้เป็นไฟล์พิเศษที่ใช้ลงทะเบียนชื่อแทนงาน ซึ่งเป็นสิ่งที่เราต้องทำก่อนเป็นส่วนหนึ่งของ Gruntfile ผ่านฟังก์ชัน registerTask นี่คือโครงการของเรา

grunt/aliases.yaml

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

เพียงเท่านี้ก็เรียบร้อยแล้ว เรียกใช้คำสั่งต่อไปนี้ในเทอร์มินัล

$ grunt

หากทุกอย่างทำงานได้ดี ตอนนี้การทำงาน "เริ่มต้น" จะดูและทำงานทุกอย่างตามลำดับ เมื่อเราตัด Gruntfile หลักให้เหลือโค้ด 3 บรรทัด เราก็ไม่จำเป็นที่จะต้องพูดถึงภายนอกทุกการกำหนดค่างาน แค่นี้ก็เรียบร้อย แต่การสร้างทุกอย่างยังค่อนข้างช้า มาดูกันว่าเราจะปรับปรุงฟีเจอร์นี้ได้อย่างไรบ้าง

ลดเวลาบิลด์

แม้ว่ารันไทม์และประสิทธิภาพเวลาที่ใช้ในการโหลดของเว็บแอปจะมีความสำคัญทางธุรกิจมากกว่าเวลาที่ใช้ในการสร้างบิลด์ แต่บิลด์ที่ช้าก็ยังคงเป็นปัญหาได้ อาจทำให้ไม่สามารถเรียกใช้บิลด์อัตโนมัติด้วยปลั๊กอิน เช่น grunt-contrib-watch หรือหลังจากที่ Git ยืนยันให้เร็วพอ ซึ่งจะมีการ "ลงโทษ" เพื่อเรียกใช้บิลด์จริง เพราะยิ่งสร้างได้เร็วเท่าไร เวิร์กโฟลว์ก็จะยิ่งมีความคล่องตัวมากขึ้นเท่านั้น หากบิลด์เวอร์ชันที่ใช้งานจริงของคุณใช้เวลาในการทำงานนานกว่า 10 นาที คุณจะเรียกใช้บิลด์เฉพาะเมื่อจำเป็นจริงๆ เท่านั้น และจะต้องออกไปซื้อกาแฟขณะทำงาน ถือว่าเป็นตัวหยุดการทำงานที่แย่ที่สุด เรายังมีอีกหลายสิ่งที่ต้องเร่งความเร็ว

สร้างเฉพาะไฟล์ที่มีการเปลี่ยนแปลงจริง: grunt-newer

หลังจากการสร้างไซต์ครั้งแรก อาจเป็นไปได้ว่าคุณได้แตะไฟล์เพียงไม่กี่ไฟล์ในโครงการเมื่อคุณกลับมาสร้างไซต์อีกครั้ง สมมติว่าในตัวอย่างของเรา คุณเปลี่ยนรูปภาพในไดเรกทอรี src/img/ การเรียกใช้ imagemin เพื่อเพิ่มประสิทธิภาพรูปภาพใหม่น่าจะเหมาะสม แต่สำหรับรูปภาพเดียวนั้นเท่านั้น และแน่นอนว่าการเรียกใช้ concat และ uglify ทำให้รอบการทำงานของ CPU สิ้นเปลือง

แน่นอนว่าคุณสามารถเรียกใช้ $ grunt imagemin จากเทอร์มินัลของคุณแทนที่จะเป็น $ grunt เพื่อเลือกดำเนินการตามที่ต้องการได้เท่านั้น แต่ก็ยังมีวิธีที่ฉลาดกว่า เรียกว่าคำใบ้ใหม่

Grunt-newer มีแคชในเครื่องที่ใช้จัดเก็บข้อมูลเกี่ยวกับไฟล์ที่มีการเปลี่ยนแปลงจริง และเรียกใช้งานของคุณสำหรับไฟล์ที่มีการเปลี่ยนแปลงจริงๆ เท่านั้น มาดูวิธีเปิดใช้งานกันเลย

จำไฟล์ aliases.yaml ของเราได้ไหม เปลี่ยนจากข้อความนี้

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

เป็น

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

เพียงใส่ "ใหม่กว่า" ไว้ล่วงหน้าในงานใดก็ได้ จะวางไฟล์ต้นทางและปลายทางผ่านปลั๊กอินใหม่กว่าปกติก่อน ซึ่งจะกำหนดว่าไฟล์ใด (หากมี) ควรทำงาน

ทำงานหลายอย่างพร้อมกัน: เสียงคำราม-พร้อมๆ กัน

grunt-concurrent เป็นปลั๊กอินที่มีประโยชน์มากเมื่อคุณมีงานจำนวนมากที่เป็นอิสระจากกันและใช้เวลามากมาย โดยจะใช้จำนวน CPU ในอุปกรณ์และทำงานหลายๆ อย่างพร้อมกัน

เหนือสิ่งอื่นใดคือการกำหนดค่านั้นง่ายมาก สมมติว่าคุณใช้load-grunt-config ให้สร้างไฟล์ใหม่ต่อไปนี้

grunt/concurrent.js

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

เราเพิ่งตั้งค่าแทร็กการดำเนินการพร้อมกันที่มีชื่อว่า "first" และ "second" ต้องเรียกใช้งาน concat ก่อนและไม่มีอะไรที่จะเรียกใช้ในระหว่างนี้ในระหว่างตัวอย่างนี้ ในแทร็กที่ 2 เราใส่ทั้ง uglify และ imagemin เนื่องจากทั้ง 2 อย่างเป็นอิสระจากกัน และใช้เวลาพอสมควร

ปัญหานี้เองยังไม่ได้ดำเนินการใดๆ เราต้องเปลี่ยนชื่อแทนงานเริ่มต้นให้ชี้ไปที่งานที่เกิดขึ้นพร้อมกันแทนงานโดยตรง นี่คือเนื้อหาใหม่ของ grunt/aliases.yaml

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

หากตอนนี้เรียกใช้บิลด์ Grunt อีกครั้ง ปลั๊กอินที่ทำงานพร้อมกันจะเรียกใช้ Concat ก่อน จากนั้นสร้างเทรด 2 รายการบน CPU 2 Core ที่ต่างกันเพื่อเรียกใช้ทั้ง imagemin และ uglify พร้อมกัน ไชโย

อย่างไรก็ตาม ขอแนะนำว่าในตัวอย่างพื้นฐานของเรานั้น เสียงสัปดอยคงไม่ได้ทำให้งานสร้างของคุณเร็วขึ้นสักเท่าไร เหตุผลก็คือค่าใช้จ่ายที่เกิดจากการสร้าง Grunt อินสแตนซ์ต่างๆ ในชุดข้อความต่างๆ กัน ในกรณีของฉัน อย่างน้อย 300 มิลลิวินาทีเกิดขึ้นได้จริง

ใช้เวลาเท่าไร กินยาบ้าแล้ว

เมื่อได้เพิ่มประสิทธิภาพทุกงานแล้ว การทำความเข้าใจว่าแต่ละงานแต่ละงานต้องใช้เวลานานเท่าใดจึงจะเป็นประโยชน์ โชคดีที่เรามีปลั๊กอินสำหรับเรื่องนี้ด้วย ซึ่งก็คือ time-grunt

Time-grunt ไม่ใช่ปลั๊กอินคำรามแบบคลาสสิกที่คุณโหลดเป็นงาน npm แต่เป็นปลั๊กอินที่คุณใส่ไว้โดยตรง ซึ่งคล้ายกับload-grunt-config เราจะเพิ่มข้อกำหนดสำหรับการอัดเสียงใน Gruntfile เช่นเดียวกับที่เราทำกับ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);

};

ขออภัยที่ต้องผิดหวัง แต่ทั้งหมดนี้คือลองเรียกใช้ Grunt อีกครั้งจากเทอร์มินัล และสำหรับทุกงาน (รวมถึงบิลด์ทั้งหมด) คุณจะเห็นแผงข้อมูลที่จัดรูปแบบอย่างสวยงามในเวลาดำเนินการ ดังนี้

เวลากรวด

การแจ้งเตือนของระบบอัตโนมัติ

ตอนนี้คุณมีบิลด์ Grunt ที่เพิ่มประสิทธิภาพอย่างหนักซึ่งทำงานได้อย่างรวดเร็วแล้ว และหากคุณสร้างบิลด์ใหม่แบบอัตโนมัติ (เช่น โดยการดูไฟล์ที่ดูมีเสียงผ่อนปรนดังขึ้น หรือหลังจากคอมมิต) จะดีแค่ไหนหากระบบของคุณสามารถแจ้งให้คุณทราบเมื่อบิลด์ใหม่พร้อมใช้งานหรือเมื่อเกิดเหตุการณ์ร้ายขึ้น พบกับgrunt-notify

โดยค่าเริ่มต้น Grunt-notify จะส่งการแจ้งเตือนอัตโนมัติสำหรับข้อผิดพลาดและคำเตือนทั้งหมดของ Grunt โดยใช้ระบบการแจ้งเตือนใดก็ได้ที่พร้อมใช้งานในระบบปฏิบัติการของคุณ ซึ่งได้แก่ Growl สำหรับ OS X หรือ Windows, Mountain Lion’s and Mavericks’ Notification Center รวมถึงการส่งแจ้งเตือน สิ่งสำคัญก็คือคุณต้องติดตั้งปลั๊กอินจาก npm แล้วโหลดลงใน Gruntfile (อย่าลืมว่าถ้าคุณใช้ grunt-load-config ด้านบนขั้นตอนดังกล่าวจะเป็นไปแบบอัตโนมัติ)

โดยจะมีลักษณะดังต่อไปนี้ โดยขึ้นอยู่กับระบบปฏิบัติการของคุณ

Notify

นอกเหนือจากข้อผิดพลาดและคำเตือนแล้ว มากำหนดค่าให้ URL ทำงานหลังจากงานล่าสุดของเราเสร็จสิ้น สมมติว่าคุณใช้ grunt-load-config เพื่อแบ่งงานระหว่างไฟล์ต่างๆ ไฟล์ที่เราต้องการมีดังนี้

grunt/notify.js

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

ในระดับแรกของออบเจ็กต์การกำหนดค่า คีย์ต้องตรงกับชื่องานที่เราต้องการเชื่อมต่อ ตัวอย่างนี้จะทำให้ข้อความปรากฏขึ้นทันทีหลังจากที่มีการดำเนินการ imagemin ซึ่งเป็นงานสุดท้ายในสายการสร้างของเรา

สรุปให้ครบ

ถ้าคุณติดตามจากระดับบน ถือว่าคุณเป็นเจ้าของกระบวนการสร้างที่แสนเรียบร้อยและเป็นระเบียบ รวดเร็วฉับไวเนื่องจากมีการทำงานแบบคู่ขนานและการประมวลผลแบบคัดสรร และจะแจ้งให้คุณทราบเมื่อมีสิ่งผิดปกติเกิดขึ้น

หากคุณค้นพบคุณลักษณะอื่นๆ ที่ช่วยปรับปรุง Grunt และปลั๊กอินเพิ่มเติม โปรดแจ้งให้เราทราบ! จนกว่าจะถึงตอนนั้น ขอให้มีความสุข!

อัปเดต (14/2/2014): หากต้องการซื้อสำเนาของโปรเจ็กต์ Grunt ตัวอย่างฉบับเต็ม ให้คลิกที่นี่