在AMD项目中使用grunt/bbb进行构建和发布

AMD项目的构建和发布

在 Javascript 前端项目中, 为了提高页面的响应速度, 通常需要对 js/css 文件进行:

  • concat, 将多个 js 或者 css 文件合成一个文件, 这样能减少浏览器给服务器发送请求的此次数, 减少在网络通讯握手上花费的时间;
  • minify, 去除 js 或者 css 文件中多余的字符, 例如注释, 换行, 空格等, 并将变量的可读的长名称改为不可读的短名称, 缩短 js 和 css 文件的长度, 减少网络传输的时间;

目前已经有很多进行 Javascript 构建和发布管理的工具, 包括Jake, Grunt, js-build-tools (ant tasks collection). RequireJS也已经提供了一个很好的优化 js 文件的工具r.js, 可以将多个 AMD 模块合并成一个文件. 这里主要介绍 Grunt 的一个 plugin 集合: Grunt-bbb

Grunt-bbb

bbb 主要包含下面这些 tasks:

  • init 初始化一个空的项目模板.
  • lint 确保所有的 js 文件符合 JSHint.
  • less 编译 LESS 生成 css 文件.
  • mincss Minify 所有的 css 文件,并合并成一个 css 文件.
  • min grunt 内置 task, Minify js 文件.
  • concat 合并多个文件到一个文件.
  • requirejs 利用r.js合并RequireJS模块到一个 js 文件.
  • server 静态文件的 HTTP 服务, 用于开发调试.

安装

1
$ npm install -g bbb

新项目初始化

进入空的项目目录,然后运行:

1
$ bbb init

根据命令行的提示输入相应的内容, 然后会生成一个空的 grunt.js 文件.

目录结构

bbb 要求按照如下目录结构组织源代码:

1
2
3
4
5
6
7
8
9
10
|-app
   |-项目 js 文件
|-dist
   |-build生成的发布文件
|-assets
   |-require.js
   |-backbone.js
   |-underscore.js 等需要的 js 库文件
|-index.html
|-favicon.ico

css 和 img 文件缺省目录在 assets/ 目录中. 我们可以更改 grunt.js 来增加/更改/删除任务(task), 以及更改任务的设置, 包括缺省目录.

AMD Backbone Contacts中增加bbb构建配置

demo 项目中没有对 js 文件进行任何优化, 下面就逐步修 改该项目来引入bbb进行项目构建和发布.

  • 修改项目目录结构, 来适应bbb的要求.

主要的改动在于将backbone, require, underscore等库文件从js/libs/移动到assets/js/目录, 将 js/目录更名为app/, 将templates/css/目录移动到assets/.

  • 增加grunt.js配置文件.

可以用bbb init命令来生成缺省的grunt.js配置文件, 必须包含下面的代码:

1
2
3
4
5
module.exports = function(grunt) {
  grunt.initConfig({
    //config options...
  });
};
  • grunt.js配置选项中定义文件路径.
1
2
3
4
5
6
7
8
  grunt.initConfig({
    //...
    dirs: {
      debug: "dist/debug", // debug files under the folder
      release: "dist/release" // release files under the folder
    },
    //...
  });
  • grunt.js 配置选项中定义文件清理任务 clean.
1
2
3
4
5
  grunt.initConfig({
    //...
    clean: ["<config:dirs.debug>", "<config:dirs.release>"],
    //...
  });
  • grunt.js配置选项中定义 lint 任务, 确保所有在 app/ 目录下的 js 文件都符合 JSHint.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  grunt.initConfig({
    //...
    lint: {
      beforeconcat: [
        "app/**/*.js"
      ],
      afterconcat: [
        "dist/debug/assets/js/require/require.js"
      ]
    },
    jshint: {
      options: {
        scripturl: true
      }
    },
    //...
  });

注:lint:afterconcat任务运行会报jQuery代码的错, 没有仔细调查原因, 因此在concat完成后没有调用lint:afterconcat任务.

  • grunt.js配置选项中定义 requirejs 任务, 合并所有项目所需要的 js 文件到一个 require.js 文件(真正的 assets/js/require/require.js文件不会包含在这个输出文件中, 后面会用concat任务将真正的require.js合并进来).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  grunt.initConfig({
    //...
    requirejs: {
      // Include the main configuration file
      mainConfigFile: "app/config.js",
      // Output file
      out: "dist/debug/assets/js/require/require.js",
      // Root application module
      name: "config",
      // Do not wrap everything in an IIFE
      wrap: false
    }
    //...
  });
  • grunt.js配置选项中定义concat任务, 将require.js库文件合并到requirejs任务的输出文件中去.
1
2
3
4
5
6
7
8
9
10
  grunt.initConfig({
    //...
    concat: {
      "dist/debug/assets/js/require/require.js": [
        "assets/js/require/require.js",
        "dist/debug/assets/js/require/require.js"
      ]
    },
    //...
  });
  • grunt.js配置选项中定义min任务, Minify concat任务生成的文件到dist/release/assets/js/require/require.js.
1
2
3
4
5
6
7
8
9
  grunt.initConfig({
    //...
    min: {
      "dist/release/assets/js/require/require.js": [
        "dist/debug/assets/js/require/require.js"
      ]
    },
    //...
  });
  • grunt.js配置选项中定义mincss任务, Minify所有的css文件到dist/release/assets/css/index.css.
1
2
3
4
5
6
7
8
  grunt.initConfig({
    //...
    mincss: {
      "dist/release/assets/css/index.css": [
        "assets/css/application.css"
      ]
    },
  });
  • grunt.js配置选项中定义copy任务, 复制相应的文件到dist/debug/或者dist/release/目录.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  grunt.initConfig({
    //...
    copy: {
      debug: {
        src: ["assets/css/**/*.css", "favicon.ico"],
        renames: {"index.noconfig.html": "index.html"},
        dest: "<config:dirs.debug>"
      },
      release: {
        src: ["favicon.ico"],
        renames: {"index.noconfig.html": "index.html"},
        dest: "<config:dirs.release>"
      }
    },
    //...
  });

bbbgrunt没有内置文件复制任务, 因此我们需要自己实现这个任务. 这里直接采用 jquery-ui的实现.

copy (copy.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Copy multitask definition, copied from jquery grunt.js file.
// It takes src/dest/rename/strip params.
module.exports = function(grunt) {

  grunt.registerMultiTask( "copy", "Copy files to destination folder and replace @VERSION with pkg.version", function() {
    function replaceVersion( source ) {
      return source.replace( /@VERSION/g, grunt.config( "pkg.version" ) );
    }
    function copyFile( src, dest ) {
      if ( /(js|css)$/.test( src ) ) {
        grunt.file.copy( src, dest, {
          process: replaceVersion
        });
      } else {
        grunt.file.copy( src, dest );
      }
    }
    var files = grunt.file.expandFiles( this.file.src ),
      target = this.file.dest + "/",
      strip = this.data.strip,
      renameCount = 0,
      fileName;
    if ( typeof strip === "string" ) {
      strip = new RegExp( "^" + grunt.template.process( strip, grunt.config() ).replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ) );
    }
    files.forEach(function( fileName ) {
      var targetFile = strip ? fileName.replace( strip, "" ) : fileName;
      copyFile( fileName, target + targetFile );
    });
    grunt.log.writeln( "Copied " + files.length + " files." );
    for ( fileName in this.data.renames ) {
      renameCount += 1;
      copyFile( fileName, target + grunt.template.process( this.data.renames[ fileName ], grunt.config() ) );
    }
    if ( renameCount ) {
      grunt.log.writeln( "Renamed " + renameCount + " files." );
    }
  });
};

grunt.js配置文件中需要加载该任务:

1
  grunt.loadTasks("tasks");  //加载所有在'tasks/'目录下的`task`.
  • grunt.js配置选项中定义server任务, 用于开发调试.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  grunt.initConfig({
    //...
    server: {
      port: 8000,
      base: ".",
      folders: {
        "app": "app",
        "assets": "assets"
      },
      debug: {
        folders: {
          "app": "dist/debug",
          "assets": "dist/debug/assets",
          "": "dist/debug"
        }
      },
      release: {
        folders: {
          "app": "dist/release",
          "assets": "dist/release/assets",
          "": "dist/release"
        }
      }
    },
    //...
  });

这里定义了3个任务:

  1. server: 侦听并返回.目录下的文件, 这个服务不对 js 和 css 文件作任何 Minify 和 concat.
  2. server:debug: 侦听并返回dist/debug/目录下的文件.
  3. server:release: 侦听并返回dist/release/目录下的文件.

  4. 将上诉任务进行串接, 定义复合任务.

1
2
3
  grunt.registerTask("default", "clean lint:beforeconcat");
  grunt.registerTask("debug", "default copy:debug requirejs concat");
  grunt.registerTask("release", "debug copy:release mincss min");
  1. default: 清除dist/目录, lint app/目录下的所有 js 文件.
  2. debug: 调用default任务, 复制相关文件到dist/debug/目录下, 合并 js(AMD模块) 文件, 最后合并require.js库文件.
  3. release: 调用debug任务, 复制相关文件到dist/release/目录下, Minify css 文件, Minify js 文件.

至此, 所有的任务都已经定义完成. 下面是完整的grunt.js文件.

gruntfile (grunt.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// This is the main application configuration file.  It is a Grunt
// configuration file, which you can learn more about here:
// https://github.com/cowboy/grunt/blob/master/docs/configuring.md
//
module.exports = function(grunt) {

  grunt.initConfig({

    // define the debug/release directories for distribution.
    // TODO, maybe remove it later. I don't like hardcode, but sometimes
    // hardcode is more simple.
    dirs: {
      debug: "dist/debug", // debug files under the folder
      release: "dist/release" // release files under the folder
    },

    // The clean task ensures all files are removed from the dist/ directory so
    // that no files linger from previous builds.
    clean: ["<config:dirs.debug>", "<config:dirs.release>"],

    // The lint task will run the build configuration and the application
    // JavaScript through JSHint and report any errors.  You can change the
    // options for this task, by reading this:
    // https://github.com/cowboy/grunt/blob/master/docs/task_lint.md
    lint: {
      beforeconcat: [
        "app/**/*.js"
      ],
      afterconcat: [
        "dist/debug/assets/js/require/require.js"
      ]
    },

    // The jshint option for scripturl is set to lax, because the anchor
    // override inside main.js needs to test for them so as to not accidentally
    // route.
    jshint: {
      options: {
        scripturl: true
      }
    },

    // The copy task is to copy files from source to distribution directory.
    copy: {
      debug: {
        src: ["assets/css/**/*.css", "favicon.ico"],
        renames: {
          "index.noconfig.html": "index.html"
        },
        dest: "<config:dirs.debug>"
      },
      release: {
        src: ["favicon.ico"],
        renames: {
          "index.noconfig.html": "index.html"
        },
        dest: "<config:dirs.release>"
      }
    },

    // The concatenate task is used here to merge the require.js into the
    // application code.  It's named dist/debug/assets/js/require/require.js,
    //because we want to only load one script file in index.html.
    concat: {
      "dist/debug/assets/js/require/require.js": [
        "assets/js/require/require.js",
        "dist/debug/assets/js/require/require.js"
      ]
    },

    // This task uses the MinCSS Node.js project to take all your CSS files in
    // order and concatenate them into a single CSS file named index.css.  It
    // also minifies all the CSS as well.  This is named index.css, because we
    // only want to load one stylesheet in index.html.
    mincss: {
      "dist/release/assets/css/index.css": [
        "assets/css/application.css"
      ]
    },

    // Takes the built require.js file and minifies it for filesize benefits.
    min: {
      "dist/release/assets/js/require/require.js": [
        "dist/debug/assets/js/require/require.js"
      ]
    },

    // Running the server without specifying an action will run the defaults,
    // port: 8080 and host: 127.0.0.1.  If you would like to change these
    // defaults, simply add in the properties `port` and `host` respectively.
    //
    // Changing the defaults might look something like this:
    //
    // server: {
    //   host: "127.0.0.1", port: 9001
    //   debug: { ... can set host and port here too ...
    //  }
    //
    //  To learn more about using the server task, please refer to the code
    //  until documentation has been written.
    // Run below commands will cause:
    // $ bbb server
    //      Run server under . folder. It uses require.js to load all needed
    //      js files, templates and css files.
    // $ bbb server:debug
    //      Run server under dist/debug folder. All js files are merged into one
    //      require.js. Make sure you run 'bbb debug' firstly.
    // $ bbb server:debug
    //      Run server under dist/release folder.
    //      All js files are merged into one require.js and minized. All css
    //      files are merged into one and minized.
    //      Make sure you run 'bbb release' firstly.
    server: {
      port: 8000,
      base: ".",
      folders: {
        "app": "app",
        "assets": "assets"
      },
      debug: {
        folders: {
          "app": "dist/debug",
          "assets": "dist/debug/assets",
          "": "dist/debug"
        }
      },
      release: {
        folders: {
          "app": "dist/release",
          "assets": "dist/release/assets",
          "": "dist/release"
        }
      }
    },

    // This task uses James Burke's excellent r.js AMD build tool.  In the
    // future other builders may be contributed as drop-in alternatives.
    requirejs: {
      // Include the main configuration file
      mainConfigFile: "app/config.js",

      // Output file
      out: "dist/debug/assets/js/require/require.js",

      // Root application module
      name: "config",

      // Do not wrap everything in an IIFE
      wrap: false
    }

  });

  // load tasks from tasks/ folder.
  grunt.loadTasks("tasks");

  // The default task will remove all contents inside the dist/ folder, lint
  // all your code.
  grunt.registerTask("default", "clean lint:beforeconcat");

  // The debug task will remove all contents inside the dist/ folder, lint all
  // js code under app/ folder, copy files to dist/debug folder, combine all
  // application files into require.js, and then concat real require.js with
  // the application require.js file.
  grunt.registerTask("debug", "default copy:debug requirejs concat");

  // The release task will perform debug task, copy files to dist/release folder,
  // minmize css file, and minimize require.js into dist/release.
  grunt.registerTask("release", "debug copy:release mincss min");
};

运行

1
$ bbb server
  • 发布文件到dist/debug/目录, 并运行 HTTP 服务器于 debug 版本:
1
2
$ bbb debug
$ bbb server:debug
  • 发布文件到dist/release/目录, 并运行 HTTP 服务器于 release 版本:
1
2
$ bbb release
$ bbb server:release

这里可以查看所有的源代码.

参考

  1. RequireJS官方网站
  2. Grunt.js
  3. bbb
  4. Grunt API
  5. jQuery-ui自定义grunt任务

Comments