Xin chào ae ! Sau khi đọc bài hướng dẫn trên Love2Dev.com về bài hướng dẫn sử dụng Grunt tôi thấy ví dụ của nó rất dễ tiếp cận để làm quen với GruntJS. Nên quyết định chia sẻ lại bài viết này cho ae đang tìm hiểu với nội dung có mô-đi-phê chút xíu cho nó phù với với dự án mà tôi đang tham gia phát triển 😀

Bàn chuyện tính cấp thiết !

Trước khi nói lan man, thì mục đích chính của bài này là hướng dẫn cho anh em hiểu 1 chút về GruntJS, thực hành 1 ví dụ đơn giản là tạo task để bó và làm “min” các file CSS và JavaScript cho ứng dụng Node.js !

GruntJS được xây dựng trên nền tảng Node.js và được quản lý bởi NPM Từ phiên bản Grunt 0.4.x yêu yều Node.js >= 0.8.0 Các phiên bản cũ của Node.js thì không ổn định ! Thông tin này ae có thể vào trực tiếp trang chủ GruntJS để xem thêm chi tiết.

Về thuật ngữ Task Runner có 1 chút khó hiểu. Cá nhân tôi hiểu nó là một tool để xây dựng Web. Vì khối lượng và độ phức tạp sẽ di chuyển theo chúng ta khi chuyển từ ứng dụng phía Server đến với ứng dụng trên các trình duyệt. Trong khi Grunt được sử dụng bởi rất nhiều dự án và thư viện web, như jQuery và Twitter. Hệ sinh thái của Grunt có hàng ngàn các plugins tiện ích có thể tự động làm thay bạn. Do vậy, Grunt sẽ là một trong những công cụ cần thiết để hỗ trợ chúng ta làm tự động một vài task, và nhiệm vụ duy nhất của ta là học cách cấu hình thằng GruntJS này !!!!

Grunt Logo

Việc làm nhỏ gọn (minification) và bó (bundling) là một trong những bước dễ bị bỏ qua nhất trong ứng dụng web hiện đại. Mặc dù phần lớn các nghiên cứu cho thấy việc gói và làm nhỏ gọn file sẽ cải thiện đáng kể thời gian tải trang web, và như bạn có thể đoán được … là có rất ít các trang web thực hiện nó. Theo HTTPArchive thì trung bình một trang web sẽ tạo 17 request tải file JavaScript và 5 file CSS. Và khi ngẫu nhiên khảo sát truy cập các trang web ngẫu nhiên thì ít khi gặp phải các trang web có nhiều hơn 50 file JavaScript và hầu hết chúng không được làm nhỏ dung lượng 🙁 Grunt thực hiện việc này rất đơn giản, loại bỏ tất cả các lý do !

Cài đặt Grunt

Cài đặt Grunt cần NodeJS đã được cài đặt trước đó, ae có thể xem hướng dẫn cài đặt node.js tại đây.

Ok, coi như ae đã cài Node.js đã thành công! Tiếp đến là cài đặt Grunt CLI bằng cách gõ lệnh sau đây trên CMD của Windows hoặc Terminal nếu là Linux OS. Việc cài đặt rất bình thường và quen thuộc:

npm install grunt-cli -g

Bạn hãy khởi tạo dự án bằng cách gõ lệnh npm init trên Terminal của bạn. Lệnh này sẽ tạo ra 1 file package.json lưu thông tin dự án và các thư viện phụ thuộc.

npm init

Bước tiếp theo là cấu hình Project bằng việc khai báo các plugins vào trong file package.json. Node sử dụng package.json để biết những modules nào sẽ được cài đặt. Sau khi cấu hình xong, chúng ta chỉ cần gõ  npm install  để cài đặt các mô-đun hoặc plugins cho dự án. Giả sử trong file package.json ta chỉ đang sử dụng Grunt là tool để dev Node, vì vậy nội dung của file chỉ chứa các dependencies là các Grunt Plugins.

Mặt mũi em nó trông như thế này:

{
    "name": "Software License Manager",
    "version": "0.1.0",
    "author": "@nhubaovu",
    "private": true,
    "devDependencies": {
        "grunt": "^0.4.5",
        "grunt-contrib-cssmin": "^0.10.0",
        "grunt-contrib-jshint": "^0.10.0",
        "grunt-contrib-qunit": "^0.5.2",
        "grunt-contrib-stylus": "^0.18.0",
        "grunt-contrib-uglify": "^0.5.1",
        "grunt-ng-annotate": "^0.3.2",
        "matchdep": "^0.3.0"
    }
}

Trên đây là nội dung file thông tin của dự án Software License Manager mà tôi đang thực hiện. Đây là dự án với mục đích là cho phép developer/operator/salesman quản lý và cấp giấy phép phần mềm viết bằng ngôn ngữ: C#, C++ và Java. Anh em có thể truy cập http://LicenseMgr.com để tham khảo !

Như các bạn có thể thấy là 4 thuộc tính đầu là thông tin của dự án: name, version, author và private. Thành phần devDependencies khai báo các node modules cần thiết để thực thi ứng dụng. Như vậy dự án LicenseMgr sử dụng 1 vài module Grunt: cssmin, qunit, jshint và uglify. Riêng module matchdep trợ giúp load tự động NpmTasks, tránh việc phải load bằng tay các grunt-* plugins.

Cấu hình Grunt

Grunt sử dụng Gruntfile.js (cùng cấp với package.json) để đọc các thông tin cấu hình cho các task runner. Các bạn ai đã quen với Node.js thì bản chất file này giống với khi chúng ta viết ra 1 mô-đun Node.js. Sau đây là 1 phiên bản Grunt tôi đang cấu hình sử dụng cho dự án LicenseMgr với các task là bó và làm nhỏ gọn các file *.css và *.js

module.exports = function (grunt) {

    // Load the Grunt plugins.
    require('matchdep')
        .filterDev('grunt-*')
        .forEach(grunt.loadNpmTasks);
    // Project configuration.
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'), cssmin: {
            sitecss: {
                options: {
                    banner: '/* LicenseMgr Minified css file */'
                }, files: {
                    'client/public/css/site.min.css': [
                        'client/public/stylesheets/app.css',
                        'client/public/stylesheets/appgrid.css',
                        'client/public/stylesheets/carousel.css',
                        'client/public/stylesheets/forms.css',
                        'client/public/stylesheets/headerfooter.css',
                        'client/public/stylesheets/home.css',
                        'client/public/stylesheets/lefCol.css',
                        'client/public/stylesheets/popup.css',
                        'client/public/stylesheets/static.css',
                        'client/public/stylesheets/style.css',
                        'client/public/stylesheets/tienX.css',
                        'client/public/stylesheets/zoom.css'
                    ],
                    'client/public/css/dashboard.min.css': [

                    ]
                }
            }
        }, uglify: {
            options: {
                report: 'min',
                compress: true,
                mangle: true
            },
            'ng-app-min': {
                files: {
                    'client/public/js/ng/site.min.js': [
                        'client/scripts/home/site.js',
                        'client/scripts/home/one.js'
                    ],
                    'client/public/js/ng/licensemgr.min.js': [
                        'client/scripts/models/routingConfig.js',
                        'client/scripts/licensemgr/one.js'
                    ]
                }
            }
        }, ngAnnotate: {
            options: {
                singleQuotes: true
            },
            app1: {
                files: {
                    'client/scripts/home/one.js': [
                        'client/scripts/home/app.js',
                        'client/scripts/home/services.js',
                        'client/scripts/home/controllers.js',
                        'client/scripts/home/filters.js',
                        'client/scripts/home/directives.js'
                    ],
                    'client/scripts/licensemgr/one.js': [
                        'client/scripts/licensemgr/app.js',
                        'client/scripts/licensemgr/services.js',
                        'client/scripts/licensemgr/controllers.js',
                        'client/scripts/licensemgr/filters.js',
                        'client/scripts/licensemgr/directives.js'
                    ]
                }
            }
        }
    });

    // Register the default tasks.
    grunt.registerTask('default', ['ngAnnotate', 'uglify', 'cssmin']);
};

OK ?? Hãy thử chạy với lệnh  grunt  trên CMD hoặc Terminal các bạn có thể thấy kết quả như sau:

Grunt Task Runner Demo

Hãy cùng phân tích từng đoạn file cấu hình Grunt ở trên. Đầu tiên module.exports là điểm bắt đầu để Grunt thực thi bằng Node.js

module.exports = function (grunt) {

    //configuration here

};

Tiếp là mô-đun matchdep được sử dụng để load toàn bộ Grunt task đã khai báo trong package.json. Nếu không có matchdep thì mỗi task cần được load bằng au-tô-mát-tay 😀 Với dự án nhỏ ko cần matchdep thì cũng được, song đối với dự án lớn thì nó là 1 vấn đề đấy ạ! Ở đây chúng ta chỉ tìm hiểu cơ bản, Grunt còn có nhiều các cấu hình phức tạp khác nữa.

    require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks);

Nếu không có matchdep, mỗi mô-đun phải được load au-tô-mát-tay như sau cho mỗi plugins có trong package.json:

    // Load au-to-mát-tay Grunt Task Plugins
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-qunit');
    grunt.loadNpmTasks('grunt-contrib-stylus');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-ng-annotate');

Phần tiếp là đoạn thực sự cấu hình Grunt. Đầu tiên là đọc file package.json. Nó sẽ sử dụng các thông tin cấu hình này để đảm bảo tất cả các plugins là mới nhất. Tiếp là cấu hình mỗi module, ở đây là các plugins mincss, uglify, ngAnnotate.

  • CSSmin: bó và làm nhỏ gọn các file *.css
  • Uglify: tương tự CSSmin cho các file *.js
  • ngAnnotate: Thêm, xóa và rebuild các ký pháp DI (dependency injection) của AngularJS

Nếu anh em nào đang viết AngularJS mà sử dụng trực tiếp Uglify để làm .min file thì dễ bị lỗi ứng dụng và không chạy được. Do đó, ta kết hợp với ngAnnotate để chuẩn hóa các DI rồi tiếp tục dùng Uglify làm min file.

    // Project configuration.  
    grunt.initConfig({  
        pkg: grunt.file.readJSON('package.json')
        , cssmin: { /* cssmin configuration */ }  
        , uglify: { /* jsmin configuration */ }
        , ngAnnotate: { /* DI explicit configuration */ }

    });

Như vậy, mỗi mô-đun sẽ có các tùy chọn cấu hình riêng, bạn cần xem các đặc tả của từng plugins trên npmjs.org hoặc github.com. Mỗi 1 plugins cần thông tin các file source cần làm min và 1 file destination (hay output file).

Cấu hình các Tasks: CSSmin, Uglify, ngAnnotate

Với CSSmin sẽ có 2 lựa chọn để định nghĩa source files và destination files. Ở đây tôi lựa chọn sử dụng đối tượng files để định nghĩa input và output files cho CSSmin. Destination file là 1 property của files và giá trị của nó chứa danh sách source files cần làm min. Như bạn thấy thì source file là 1 mảng được sắp xếp theo thứ tự được bó gộp lại với nhau trong output file.

Chú ý là tôi có 1 file  ‘client/public/css/dashboard.min.css’  chưa định nghĩa xong, nhưng nó không vấn đề gì cả. Task vẫn thực hiện ok

sitecss: {
    options: {
        banner: '/* LicenseMgr Minified css file */'
    }, files: {
        'client/public/css/site.min.css': [
            'client/public/stylesheets/app.css',
            'client/public/stylesheets/appgrid.css',
            'client/public/stylesheets/carousel.css',
            'client/public/stylesheets/forms.css',
            'client/public/stylesheets/headerfooter.css',
            'client/public/stylesheets/home.css',
            'client/public/stylesheets/lefCol.css',
            'client/public/stylesheets/popup.css',
            'client/public/stylesheets/static.css',
            'client/public/stylesheets/style.css',
            'client/public/stylesheets/tienX.css',
            'client/public/stylesheets/zoom.css'
        ],
        'client/public/css/dashboard.min.css': [
            // Source files here for new !!!!
        ]
    }
}

Tương tự thì mô-đun uglify cũng cần được cấu hình. Ở đây, uglify có 1 option là có nén file, chi tiết về uglify hãy đọc tài liệu của nó. Trong task này định nghĩa 1 task con là ‘ng-app-min’ chứa thông tin các file javascripts. Tương tự với mô-đun cssmin, output sẽ được bó và làm min theo thứ tự ta sắp xếp. Hãy nhớ là đặt file có mức độ phụ thuộc cao nhất ở đầu tiên.

uglify: {
    options: {
        report: 'min',
        compress: true,
        mangle: true
    },
    'ng-app-min': {
        files: {
            'client/public/js/ng/site.min.js': [
                'client/scripts/home/site.js',
                'client/scripts/home/one.js'
            ],
            'client/public/js/ng/licensemgr.min.js': [
                'client/scripts/models/routingConfig.js',
                'client/scripts/licensemgr/one.js'
            ]
        }
    }
}

Task cuối cùng là ngAnnotate, đồng chí này sẽ có nhiệm vụ là gộp toàn bộ mã nguồn Angular vào làm 1 file, đồng thời, sẽ chuẩn hóa các DI để khi task làm min file uglify không làm ứng dụng AngualarJS bị lỗi: Ví dụ là nó khai báo $service trước khi sử dụng trong Controllers, Directives, … và đổi ký tự “nhép kép” thành ‘nháy đơn’.

ngAnnotate: {
    options: {
        singleQuotes: true
    },
    app1: {
        files: {
            'client/scripts/home/one.js': [
                'client/scripts/home/app.js',
                'client/scripts/home/services.js',
                'client/scripts/home/controllers.js',
                'client/scripts/home/filters.js',
                'client/scripts/home/directives.js'
            ],
            'client/scripts/licensemgr/one.js': [
                'client/scripts/licensemgr/app.js',
                'client/scripts/licensemgr/services.js',
                'client/scripts/licensemgr/controllers.js',
                'client/scripts/licensemgr/filters.js',
                'client/scripts/licensemgr/directives.js'
            ]
        }
    }
}

Bước cuối cùng là ta đăng ký 1 task default theo thứ tự thực hiện là ‘ngAnnotate’, ‘uglify’, rồi đến ‘cssmin’. Về thứ tự là rất quan trọng vì task ‘uglify’ chỉ được thực hiện khi task ‘ngAnnotate’ được thực hiện vì nó là input của task ‘uglify’.


    // Register the default tasks.
    grunt.registerTask('default', ['ngAnnotate', 'uglify', 'cssmin']);

Trên đây là những thứ cơ bản để bạn có thể thực hiện build được 1 hệ thống web tự động. Hướng dẫn cho ae hiểu được cách bundle và minify, nhưng Grunt nó còn rất nhiều thứ khác nữa mà có thể thực hiện được tự động. Việc cấu hình trong hướng dẫn này rất đơn giản, nếu bạn đã thành thạo rồi thì chỉ mất từ 5 đến 10 phút để cài đặt. Về chất lượng việc làm bạn thấy là từ 12 file css xuống còn 1 file và dung lượng còn 1 nửa. Tương tự với file Javascript. Điều này giúp cho website của chúng ta cải thiện đáng kể về tốc độ và hiệu suất tải trang.

Vọc + tự sướng thôi nào !!!!!!

About The Author