前端工程之构建工具-gulp实战

@kuitos 2015-08-23 09:38:36发表于 kuitos/kuitos.github.io 工具

前端工程之构建工具-gulp实战

原文写于 2014-10-24

对前端构建自动化有所了解的同学应该都听过大名鼎鼎的grunt,但是grunt作为前端自动化构建工具的领头羊现在越来越受到诟病。大而全已经不适应这个时代了,这个时代需要的是小而美的工具及插件。什么都想干往往意味着什么都干不好,YUI的死掉至少说明了近年前端的发展趋势。grunt的不足及弊端大家可以参考这篇文章。后起之秀gulp大有取代grunt的趋势(gulp的github的stars目前已经超过grunt了),本文主要介绍如何通过gulp实现自动化压缩、合并以及文件的修改。
首先来说说我们想要实现的功能吧

  • 最基础的js、css合并压缩(html看个人爱好...不过有坑请慎重考虑...)
  • 开发环境在/src目录下,发布环境在/dist目录下。开发人员只需要维护/src目录代码,/dist目录不应该有任何的人为干预(人工不靠谱,出错查不出...)
  • 发布时不需要手动更新静态资源版本号。如果我们的js、css有改动,我们不能说每次发布后都告诉用户去清缓存才能更新代码。古老的做法是每次发布时加上版本号,类似<script src=xxx.js?v=0.1></script>。这样太麻烦。合理的应该是,只要我们的源码有改动,那么我们引用的资源版本号就应该自动更新。

来看看我们怎样通过gulp实现我们的前端工程自动化的吧
首先是一个源码层级的index.html,差不多长这样

<!--
 Created by kui.liu on 2014/05/29 14:34.
-->
<!DOCTYPE html>
<html ng-app="invoiceApp">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

    一堆css文件。。。。
    <!-- build:css css/app.css -->
    <link href="/src/css/bootstrap-tooltips.css" rel="stylesheet">
    <link href="/src/js/lib/jquery-ui/development-bundle/themes/base/jquery.ui.theme.css" rel="stylesheet">
    <link href="/src/js/lib/jquery-ui/development-bundle/themes/base/jquery.ui.core.css" rel="stylesheet">
    <link href="/src/js/lib/jquery-ui/development-bundle/themes/base/jquery.ui.datepicker.css" rel="stylesheet">
    <link href="/src/js/lib/jQuery-Timepicker-Addon/dist/jquery-ui-timepicker-addon.min.css" rel="stylesheet">
    <link href="/src/css/float-tips.css" rel="stylesheet"/>
    <link href="/src/css/ccms.css" rel="stylesheet">
    <link href="/src/css/app.css" rel="stylesheet">
    <link href="/src/js/lib/ccms_pop/css/style.css" rel="stylesheet">
    <!-- endbuild -->

    <style>
        [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
            display: none !important;
        }
    </style>
    <title>账户管理</title>
    <script>
        //  设置资源文件目录,必需!!
        window.ResourceDir = "/src/";
    </script>
</head>
<body ng-cloak class="ng-cloak" ng-controller="AppCtrl"> 
    此处省略若干结构。。。
    一堆js文件
    <!-- build:js js/app.js -->
    <script src="/src/js/lib/jquery/jquery-1.11.1.js"></script>
    <script src="/src/js/lib/jquery-ui/development-bundle/ui/jquery.ui.core.js"></script>
    <script src="/src/js/lib/jquery-ui/development-bundle/ui/jquery.ui.datepicker.js"></script>
    <script src="/src/js/lib/jQuery-Timepicker-Addon/dist/jquery-ui-timepicker-addon.min.js"></script>
    <script src="/src/js/lib/jQuery-Timepicker-Addon/dist/i18n/jquery-ui-timepicker-zh-CN.js"></script>
    <script src="/src/js/lib/bootstrap/bootstrap-tooltips.js"></script>
    <script src="/src/js/lib/ccms_pop/js/yunat_pop.js"></script>
    <script src="/src/js/float-tips.js"></script>
    <script src="/src/js/lib/angular/angular.js"></script>
    <script src="/src/js/lib/angular/angular-locale_zh-cn.js"></script>
    ...........
    <!-- endbuild -->
</body>
</html>

然后是我们开发时的目录结构
image2014-10-26 15:30:4.png
然后我使用gulp定义了一系列构建任务后,执行一下命令 gulp build (假设你也写了个名为build的任务)
我们的工程变成这样了
首先是工程目录
image2014-10-26 15:32:40.png
然后是/dist/index.html

<!--
 Created by kui.liu on 2014/05/29 14:34.
-->
<!DOCTYPE html>
<html ng-app="invoiceApp">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">


    <link rel="stylesheet" href="/dist/css/app-cb402b9f.css"/>

    <style>
        [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
            display: none !important;
        }
    </style>
    <title>账户管理</title>
    <script>
        //  设置资源文件目录,必需!!
        window.ResourceDir = "/dist/";
    </script>
</head>
<body ng-cloak class="ng-cloak" ng-controller="AppCtrl">
    ........
    <script src="/dist/js/app-3db90639.js"></script>
</body>
</html>

可以看到,/dist目录如期而至,而且长得跟我们预想的一样,然后是/dist/index.html,所有的css和js引用均指向一个压缩合并后的文件,眼尖的同学会发现,后面跟的那一串奇怪的数字是啥,如
app-3db90639.js。中划线后面那串数字是我们通过gulp计算了合并文件的MD5之后算出的hash值,也就是说,只要你的源码有改动,那么这个hash值便会自动追加到合并文件后面,而不需要你每次手动修改版本号那种古老的做法,一切都是gulp帮我们做好,我们需要做的就是敲一行命令就好了。赞一个!

下面具体介绍一下gulp怎么玩才能达到上文的效果

  1. 首先你要有个nodejs环境。nodeJs

  2. 安装gulp、bower(假设你有用到)等node组件

    node install gulp -g
    node install gulp --save-dev
  3. 引入gulp合并、压缩等插件(package.json文件)

    "devDependencies": {
        "gulp"            : ">=3.8.8",
        "del"             : ">=0.1.3",
        "gulp-minify-css" : ">=0.3.10",
        "gulp-minify-html": ">=0.1.6",
        "gulp-replace"    : ">=0.4.0",
        "gulp-rev"        : ">=1.1.0",
        "gulp-uglify"     : ">=1.0.1",
        "gulp-usemin"     : ">=0.3.8",
        "run-sequence"    : ">=1.0.1"
    }
  4. 构建gulp任务流,就像这样

    /**
     * @author kui.liu
     * @since 2014/09/29 上午11:45
     */
    "use strict";
    var webRoot = "src/main/webapp/",
        gulp = require('gulp'),
        del = require('del'),
        runSequence = require('run-sequence'),
        minifyCss = require('gulp-minify-css'),
        minifyHtml = require('gulp-minify-html'),
        uglify = require('gulp-uglify'),
        rev = require('gulp-rev'),
        replace = require('gulp-replace'),
        usemin = require('gulp-usemin');
    /*------------------------- 清空dist目录 -----------------------------*/
    gulp.task('clean', function (cb) {
        del(webRoot + "dist/*", cb);
    });
    /*------------------------- 拷贝资源文件 -----------------------------*/
    gulp.task('copy-tpl', function () {
        return gulp.src(webRoot + 'src/tpls/**/*.html')
            //        .pipe(minifyHtml({empty: true, quotes: true}))
            .pipe(gulp.dest(webRoot + 'dist/tpls'));
    });
    gulp.task('copy-font', function () {
        return gulp.src(webRoot + 'src/fonts/*')
            .pipe(gulp.dest(webRoot + 'dist/fonts/'));
    });
    gulp.task('copy-image', function () {
        return gulp.src(webRoot + 'src/images/**/*')
            .pipe(gulp.dest(webRoot + 'dist/images'));
    });
    gulp.task('copy-jqueryui-image', function () {
        return gulp.src(webRoot + 'src/js/lib/jquery-ui/development-bundle/themes/base/images/*')
            .pipe(gulp.dest(webRoot + 'dist/css/images'));
    });
    
    /*------------------------- 对首页引用的css、js作合并压缩,并根据文件md5值自动更新 -----------------------------*/
    /*------------------------- 首页html需要加入build标识,具体参照gulp-usemin插件文档 -----------------------------*/
    gulp.task('usemin', function () {
        return gulp.src(webRoot + 'src/index.html')
            .pipe(replace(/\/src\/(js|css)/g, '$1'))
            .pipe(usemin({
                css: [minifyCss({keepSpecialComments: 0}), rev()],
                //            html: [minifyHtml({empty: true, quotes: true})],
                js : [uglify(), rev()]
            }))
            .pipe(replace(/(js\/|css\/)/g, "/dist/$1"))
            .pipe(replace(/\/src\//g, "/dist/"))
            .pipe(gulp.dest(webRoot + 'dist/'));
    });
    // 定义构建任务队列
    gulp.task('build', function (cb) {
        runSequence('clean', ['copy-tpl', 'copy-font', 'copy-image', 'copy-ccmsPop-image', 'copy-jqueryui-image', 'usemin'], cb);
    });

    gulp采用管道流机制定义任务,任务的定义跟使用变得更易用和清晰。要运行单个定义的任务很简单,比如上面定义的clean任务:

    gulp clean

    一般情况下我们不会只执行一个构建任务,如果要一个个任务去敲命令显然是效率低下的,这里引入runSequence插件,从而自定义任务队列,比如上面我们定义build任务用来执行所有的合并、压缩的任务,执行方式跟其他任务一样:

    gulp build

    有的时候我们还用了bower等其他工具,不仅仅是gulp。那么构建工程的时候需要bower install,然后gulp build。懒人总想找到更简单的方式,我们在package.json中加入这个

    "scripts"        : {
        "start": "npm install & bower install --allow-root & gulp build"
    }

    这样我们构建项目只需要一个命令就OK

    npm start

    命令跑完之后一切就都变成了你想要的模样。就是这么简单。so easy!麻麻再也不用担心我每次发布都要小心翼翼了。

ps:由于我们node配置文件是放在项目根路径下的,而maven只打包webapp下的资源,所以如果你不想每次发布前还要手动跑node命令然后提交编译目录,配合bamboo可以在maven打包前加上脚本命令,如图
image2014-10-27 10:7:27.png

更多详细代码请移步:source code