Tạo hiệu ứng cho video

Cách khai thác tối đa cấu hình bản dựng

Paul Bakaus
Paul Bakaus

Giới thiệu

Nếu thế giới Grunt còn mới mẻ đối với bạn, thì nơi lý tưởng để bắt đầu là bài viết xuất sắc của Chris Coyier “Grunt cho những người nghĩ những điều như Grunt là kỳ lạ và khó xử”. Sau phần giới thiệu của Chris, bạn sẽ thiết lập dự án Grunt của riêng mình và trải nghiệm một phần sức mạnh mà Grunt cung cấp.

Trong bài viết này, chúng tôi sẽ không tập trung vào những lợi ích của nhiều trình bổ trợ Grunt cho mã dự án thực tế của bạn, mà tập trung vào chính quy trình xây dựng Grunt. Chúng tôi sẽ đưa ra cho bạn các ý tưởng thiết thực về:

  • Cách giữ cho Gruntfile của bạn gọn gàng và ngăn nắp,
  • Cách cải thiện đáng kể thời gian xây dựng,
  • Và cách nhận thông báo khi có quá trình xây dựng.

Đã đến lúc để tuyên bố từ chối trách nhiệm ngắn gọn: Grunt chỉ là một trong số nhiều công cụ bạn có thể sử dụng để hoàn thành nhiệm vụ. Nếu Gulp phù hợp với phong cách của bạn thì thật tuyệt! Nếu sau khi khảo sát các phương án mà bạn vẫn muốn xây dựng cho riêng mình, thì cũng không sao! Chúng tôi chọn tập trung vào Grunt cho bài viết này do hệ sinh thái vững mạnh và cơ sở người dùng lâu dài của Grunt.

Sắp xếp Gruntfile

Cho dù bạn đưa nhiều trình bổ trợ Grunt hay phải viết nhiều tác vụ thủ công trong Gruntfile, thì trình bổ trợ này có thể nhanh chóng trở nên rất khó sử dụng và khó duy trì. May mắn là có khá nhiều trình bổ trợ tập trung vào chính xác vấn đề đó: Giúp Gruntfile của bạn gọn gàng và ngăn nắp trở lại.

Gruntfile, trước khi tối ưu hoá

Sau đây là giao diện của Gruntfile trước khi chúng tôi thực hiện bất kỳ hoạt động tối ưu hoá nào trên đó:

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']);

};

Nếu bây giờ bạn nói "Xin chào! Mong là tôi tệ hơn nhiều! Điều đó thực sự có thể duy trì được!", có lẽ bạn đã đúng. Để đơn giản, chúng tôi chỉ bao gồm 3 trình bổ trợ mà không cần tuỳ chỉnh nhiều. Nếu dùng Gruntfile trong quá trình sản xuất thực tế thì để xây dựng một dự án có quy mô vừa phải, bạn sẽ phải cuộn vô hạn trong bài viết này. Vì vậy, hãy xem chúng ta có thể làm gì!

Tự động tải các trình bổ trợ Grunt

Khi thêm một trình bổ trợ Grunt mới mà bạn muốn sử dụng vào dự án, bạn sẽ phải thêm cả hai trình bổ trợ này vào tệp package.json dưới dạng phần phụ thuộc npm rồi tải trình bổ trợ đó trong Gruntfile. Đối với trình bổ trợ “grunt-contrib-concat”, trình bổ trợ này có thể có dạng như sau:

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

Nếu hiện bạn gỡ cài đặt trình bổ trợ thông qua npm và cập nhật package.json, nhưng quên cập nhật tệp Gruntfile, thì bản dựng của bạn sẽ bị lỗi. Đây là lúc trình bổ trợ tiện lợi load-grunt-tasks sẽ trợ giúp.

Trong khi trước đây, chúng ta phải tải thủ công các trình bổ trợ Grunt, như sau:

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

Với load-grunt-tasks, bạn có thể thu gọn nội dung đó xuống nội dung một lớp sau:

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

Sau khi yêu cầu trình bổ trợ, Chrome sẽ phân tích tệp package.json của bạn, xác định xem phần phụ thuộc nào là trình bổ trợ Grunt và tự động tải tất cả những phần phụ thuộc đó.

Chia cấu hình Grunt thành nhiều tệp

load-grunt-tasks giúp Gruntfile rút gọn mã và độ phức tạp một chút, nhưng khi bạn định cấu hình một ứng dụng lớn, tệp đó vẫn sẽ trở thành một tệp rất lớn. Đây là nơi load-grunt-config phát huy tác dụng. load-grunt-config cho phép bạn chia nhỏ cấu hình Gruntfile theo tác vụ. Hơn nữa, nó còn đóng gói các tác vụ tải-grunt và chức năng của nó!

Tuy nhiên, quan trọng: Việc tách Gruntfile có thể không phải lúc nào cũng hiệu quả trong mọi trường hợp. Nếu có nhiều cấu hình dùng chung giữa các tác vụ (tức là sử dụng nhiều mẫu Grunt), bạn nên cẩn thận một chút.

Với load-grunt-config, tệp Gruntfile.js của bạn sẽ có dạng như sau:

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

Vâng, đó thực sự là toàn bộ tệp! Bây giờ cấu hình tác vụ của chúng ta nằm ở đâu?

Tạo một thư mục có tên grunt/ trong thư mục của Gruntfile. Theo mặc định, trình bổ trợ sẽ bao gồm các tệp trong thư mục đó và khớp với tên của tác vụ bạn muốn sử dụng. Cấu trúc thư mục của chúng ta sẽ có dạng như sau:

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

Bây giờ, hãy trực tiếp đặt cấu hình tác vụ của từng tác vụ vào các tệp tương ứng (bạn sẽ thấy rằng các tệp này chủ yếu chỉ là sao chép và dán từ Gruntfile ban đầu vào một cấu trúc mới):

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/'
    }]
  }
};

Nếu các khối cấu hình JavaScript không thực sự là sở thích của bạn, load-grunt-tasks thậm chí còn cho phép bạn sử dụng cú pháp YAML hoặc CoffeeScript. Hãy viết tệp cần thiết cuối cùng trong YAML – tệp “aliases”. Đây là một tệp đặc biệt giúp đăng ký các bí danh của tác vụ mà chúng ta phải làm trong Gruntfile trước đó thông qua hàm registerTask. Sau đây là những công việc của chúng tôi:

grunt/aliases.yaml

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

Chỉ vậy thôi! Thực thi lệnh sau trong dòng lệnh của bạn:

$ grunt

Nếu mọi thứ hoạt động thì giờ đây, tác vụ "mặc định" sẽ được xem xét và chạy mọi thứ theo đúng thứ tự. Bây giờ, chúng ta đã rút gọn Gruntfile chính xuống còn ba dòng mã mà chúng ta không bao giờ cần đụng đến và tách riêng mọi cấu hình tác vụ, chúng ta đã hoàn tất ở đây. Nhưng mọi người vẫn còn khá chậm để xây dựng mọi thứ. Hãy xem chúng ta có thể làm gì để cải thiện điều đó.

Giảm thiểu thời gian xây dựng

Mặc dù hiệu suất thời gian chạy và thời gian tải của ứng dụng web quan trọng hơn nhiều so với thời gian cần thiết để thực thi một bản dựng, nhưng bản dựng chậm vẫn là một vấn đề. Điều này sẽ khiến việc thực thi các bản dựng tự động bằng các trình bổ trợ như grunt-contrib-watch hoặc sau khi Git xác nhận đủ nhanh và đưa ra một "hình phạt" để thực sự chạy bản dựng – thời gian tạo bản dựng càng nhanh, quy trình làm việc của bạn càng linh hoạt. Nếu bản dựng chính thức của bạn mất hơn 10 phút để chạy, bạn sẽ chỉ chạy bản dựng khi thực sự cần thiết và bạn sẽ đi đây đó để mua cà phê trong khi chạy. Quả là một yếu tố tăng năng suất làm việc. Chúng tôi cần đẩy nhanh tiến độ thực hiện một số việc.

Chỉ tạo các tệp thực sự thay đổi: grunt-newer

Sau khi tạo trang web ban đầu, có khả năng bạn chỉ cần thao tác với một vài tệp trong dự án khi bắt đầu xây dựng lại. Giả sử trong ví dụ này, bạn đã thay đổi một hình ảnh trong thư mục src/img/ – việc chạy imagemin để tối ưu hoá lại hình ảnh là hợp lý, nhưng chỉ đối với hình ảnh duy nhất đó – và tất nhiên, việc chạy lại concatuglify chỉ làm lãng phí các chu kỳ quý giá của CPU.

Tất nhiên, bạn luôn có thể chạy $ grunt imagemin từ dòng lệnh thay vì $ grunt để chỉ thực thi một cách có chọn lọc một tác vụ hiện có, nhưng có một cách thông minh hơn. Đó là grunt-newer (mới hơn).

Grunt-newer có một bộ nhớ đệm cục bộ lưu trữ thông tin về những tệp đã thực sự thay đổi và chỉ thực thi các tác vụ của bạn đối với những tệp đã thay đổi trên thực tế. Hãy tìm hiểu cách kích hoạt tính năng này.

Bạn có nhớ tệp aliases.yaml không? Bạn có thể thay đổi từ:

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

tới đây:

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

Chỉ cần đặt trước "newer:" (mới hơn): trong bất kỳ tác vụ nào của bạn, sẽ chuyển tệp nguồn và tệp đích của bạn thông qua trình bổ trợ grunt-newer (trình bổ trợ mới hơn), sau đó sẽ xác định những tệp nào, nếu có, tác vụ sẽ chạy.

Chạy nhiều tác vụ song song: grunt-concurrent

grunt-concurrent là một trình bổ trợ trở nên thực sự hữu ích khi bạn có nhiều tác vụ độc lập với nhau và tốn nhiều thời gian. Mô hình này sử dụng số lượng CPU trên thiết bị của bạn và thực thi song song nhiều tác vụ.

Hơn hết, cấu hình của tiện ích này cực kỳ đơn giản. Giả sử bạn sử dụng load-grunt-config, hãy tạo tệp mới như sau:

grunt/concurrent.js

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

Chúng ta chỉ thiết lập cho các kênh thực thi song song có tên "first" và "second". Tác vụ concat cần phải chạy trước và không có nội dung nào khác để chạy trong thời gian chờ trong ví dụ của chúng ta. Trong kênh thứ hai, chúng ta đặt cả uglifyimagemin, vì 2 phương diện này độc lập với nhau và chiếm một lượng thời gian đáng kể.

Bản thân việc này vẫn chưa có tác dụng gì. Chúng ta phải thay đổi bí danh tác vụ mặc định để trỏ đến các công việc đồng thời thay vì các công việc trực tiếp. Đây là nội dung mới của grunt/aliases.yaml:

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

Nếu hiện tại bạn chạy lại bản dựng grunt của mình, trình bổ trợ đồng thời sẽ chạy tác vụ concat trước, sau đó tạo hai luồng trên hai lõi CPU khác nhau để chạy cả imagemin và uglify song song. Thật tuyệt vời!

Tuy nhiên, một lời khuyên là: Có khả năng là trong ví dụ cơ bản của chúng ta, hiện tượng grunt-concurrent sẽ không giúp bản dựng của bạn nhanh hơn đáng kể. Lý do cho điều này là chi phí được tạo ra bằng cách tạo ra các phiên bản khác nhau của Grunt trong các chuỗi khác nhau: Trong trường hợp của tôi, ít nhất là +300 mili giây prown.

Mất bao nhiêu thời gian?

Hiện tại, chúng ta đang tối ưu hoá từng nhiệm vụ của mình, nên sẽ rất hữu ích nếu biết được mỗi nhiệm vụ riêng lẻ cần bao nhiêu thời gian để thực hiện. May mắn là có một trình bổ trợ giúp bạn làm việc này: time-grunt.

time-grunt không phải là một trình bổ trợ grunt cổ điển mà bạn tải dưới dạng tác vụ npm, mà là một trình bổ trợ mà bạn trực tiếp đưa vào, tương tự như load-grunt-config. Chúng ta sẽ thêm yêu cầu về thời gian vào Gruntfile, giống như đã thực hiện với load-grunt-config. Gruntfile của chúng ta sẽ có dạng như sau:

module.exports = function(grunt) {

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

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

};

Tôi rất tiếc khi bạn phải thất vọng, nhưng thế là xong – hãy thử chạy lại Grunt từ Terminal của bạn và đối với mọi nhiệm vụ (và cả tổng số bản dựng), bạn sẽ thấy bảng thông tin được định dạng độc đáo về thời gian thực thi:

Giờ thình lình

Thông báo tự động của hệ thống

Giờ đây, khi bạn đã có một bản dựng Grunt được tối ưu hoá mạnh mẽ và có thể thực thi nhanh chóng và miễn là bạn tự động tạo bản dựng này theo một cách nào đó (tức là bằng cách xem các tệp bằng chế độ grunt-contrib-watch hoặc sau khi thay đổi), thì thật tuyệt nếu hệ thống có thể thông báo cho bạn thời điểm bản dựng mới sẵn sàng sử dụng hoặc khi có vấn đề gì xảy ra? Gặp grunt-notify.

Theo mặc định, tính năng thông báo tự động cung cấp thông báo tự động cho tất cả các lỗi và cảnh báo của Grunt bằng bất kỳ hệ thống thông báo nào có sẵn trên hệ điều hành của bạn: Growl dành cho OS X hoặc Windows, Trung tâm thông báo của Mountain Lion và Mavericks và Thông báo cho tính năng gửi. Thật tuyệt vời, tất cả những gì bạn cần để có được chức năng này là cài đặt trình bổ trợ từ npm và tải trình bổ trợ đó trong Gruntfile (hãy nhớ rằng nếu bạn đang sử dụng grunt-load-config ở trên, thì bước này được tự động hoá!).

Tuỳ thuộc vào hệ điều hành mà bạn sử dụng, giao diện sẽ có dạng như sau:

Notify

Ngoài các lỗi và cảnh báo, hãy định cấu hình để mã này chạy sau khi tác vụ cuối cùng của chúng ta hoàn tất quá trình thực thi. Giả sử bạn đang sử dụng grunt-load-config để chia nhỏ tác vụ trên nhiều tệp, thì đây là tệp chúng ta sẽ cần:

grunt/notify.js

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

Ở cấp đầu tiên của đối tượng cấu hình, khoá phải khớp với tên của tác vụ mà chúng ta muốn kết nối. Ví dụ này sẽ khiến thông báo xuất hiện ngay sau khi tác vụ imagemin được thực thi. Đây là tác vụ cuối cùng trong chuỗi bản dựng của chúng ta.

Tổng kết

Nếu bắt đầu từ trên xuống, bạn tự hào là chủ nhân của một quy trình xây dựng cực kỳ gọn gàng và ngăn nắp, cực kỳ nhanh chóng nhờ việc xử lý song song và xử lý có chọn lọc, đồng thời thông báo cho bạn khi có sự cố.

Nếu bạn phát hiện thêm một viên đá quý khác có thể cải thiện Grunt và các trình bổ trợ của Grunt, vui lòng cho chúng tôi biết! Cho đến lúc đó, chúc bạn vui vẻ!

Cập nhật (14/2/2014): Để lấy bản sao của dự án Grunt đầy đủ và hoạt động tốt, hãy nhấp vào đây.