2016 年 11 月

第 31 卷,第 11 期

孜孜不倦的程序员 - 如何成为 MEAN: 采用 Gulp

作者 Ted Neward | 2016 年 11 月

Ted Neward欢迎回来,MEAN。

如果你阅读了我在十月份发表的上一篇专栏文章(其中讨论了基本代码的“重启”,方法是使用 Yeoman 搭建基础和 glue 代码的基架),你可能已注意到文章中引入了一个新工具,但未对此工具作出过多阐述。当然,我指的是搭建基架的客户端应用程序中引入的“Gulp”工具(用于启动服务器和打开客户端浏览器)(msdn.com/magazine/mt742874)。

如果你没有阅读我的上一篇专栏文章,也能很容易地理解本文内容。首先,确保在 Node.js 开发环境中安装了 Yeoman 和“angular-fullstack”生成器(以及 MongoDB 的本地运行副本):

npm install –g yeoman angular-fullstack-generator
yo angular-fullstack

然后,基架搭建程序将回答 Yeoman 应将基架搭建到哪些工具中的问题(对于本专栏文章,不论你做何选择,基本都无关紧要),并且 Yeoman 会启动“npm install”以关闭所有运行时和开发依赖项(这一点很有用),然后基架搭建程序将会运行“gulp test”或“gulp start:server”,报告应用程序已准备就绪可进行测试。

显然,无论 Gulp 是什么,它是一种生成工具,类似于 Make、MSBuild 或 Ant。但是它的工作原理与以上三种工具均略有差异,因此值得进行讨论。

首先介绍一点 Gulp

尽管对于从未生成的语言来说,将 Gulp 称为“生成工具”有点不公平(请记住,ECMAScript 通常是一种解释语言),此术语非常适用于旨在开发后(或开发期间)运行的工具以确保一切有条不紊、准备就绪。或许更加适合它们的名称是“开发自动化工具”,因为这更加准确且包括编译代码和将其汇编到可部署项目的操作。但是,因为这个名称又长又拗口,使用“生成工具”会比较顺口,所以我们就暂且将 Gulp 视为生成工具吧。

要开始使用 Gulp,让我们摆脱过去搭建基架的代码,从头开始关注 Gulp 可实现的操作及其实现方式。首先,安装全局 Gulp 命令行工具 (“npm install --g gulp-cli”)。然后,在新目录中通过运行以下命令创建空 Node.js 项目:

npm init
npm install --save-dev gulp

这将确保在 package.json 文件中将 Gulp 引用为开发者依赖项,因此,如果关闭项目,也不必记住要安装 Gulp—它将在新环境中随下一个“npm install”一起安装。

现在,在所选的文本编辑器中,创建一个新文件,称之为 gulpfile.js:

const gulp = require('gulp');
gulp.task('default', function() {
  console.log("Gulp is running!");
});

然后,从相同的目录中发起 stock Gulp 命令,这(不出所料)就是“gulp”。 Gulp 会稍作处理,然后返回:

[18:09:38] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[18:09:38] Starting 'default'...
Gulp is running!
[18:09:38] Finished 'default' after 142 μs

真不错。现在似乎有点不必要,但还算不错。

使用 Gulp 处理一些任务

从“任务”的方面考虑,Gulp 更加类似于生成工具,更重要的是,如何在这些任务之间跟踪依赖项。因此,在此处简单 Gulpfile 中,似乎只有一个任务(称为“default”,这是一个很好理解的约定,意味着如果命令行中未指定任何内容,应执行此任务),当要求执行该任务时,执行关联函数文本的主体。有区别地命名任务所起的作用是微不足道的:

const gulp = require('gulp');
gulp.task('echo', function() {
  console.log("Gulp is running!");
});

很明显 Gulp 只是代码,因此可在代码中执行的任何操作均可在 Gulp 任务的主体中执行。这提供了许多令人难以置信的选项,如从数据库读取以查找需要进行代码生成的元素、与其他联机服务通信以进行配置,或者甚至仅打印出当前日期和时间:

const gulp = require('gulp');
gulp.task('echo', function() {
  console.log("Gulp is running on " + (new Date()));
});

这会生成以下内容:

Teds-MacBook-Pro:gulpdemo tedneward$ gulp echo
[18:16:24] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[18:16:24] Starting 'echo'...
Gulp is running on Wed Sep 14 2016 18:16:24 GMT-0700 (PDT)
[18:16:24] Finished 'echo' after 227 μs

在任务名称及其函数文本主体之间以字符串数组形式简单列出相关任务,因此使任务根据另一个任务进行“回显”的操作就如下所示:

const gulp = require('gulp');
const child_process = require('child_process');
gulp.task('gen-date', function() {
  child_process.exec('sh date > curdate.txt');
});
gulp.task('echo', ['clean', 'gen-date'], function() {
  console.log("Gulp is running on " + (new Date()));
});

此处,“gen-date”任务使用标准 Node.js 包(“child_process”)来启动外部工具以将日期写入文件,只是为了证明可以执行此操作。这确实完全没有问题,但一般希望使用生成工具执行一些更加重要的操作,而不仅是将一些内容写入控制台并算出当前日期和时间。

进一步利用 Gulp

让我们来丰富一下内容。在其中使用以下 ECMAScript code 代码创建 index.js 文件,包括不甚理想的代码部分:

// index.js
function main(args) {
  for (let arg in args) {
    if (arg == "hello")
      console.log("world!");
      console.log("from index.js!");
    }
}
console.log("Hello, from index.js!")
main()

是的,这有点荒谬并且显然存在一些问题,但这是关键所在—如果有工具可发现部分这些问题并报告这些问题就好了。(容我这样说,“编译时检查”?) 幸运的是,JSHint (jshint.com) 中存在此类工具,但默认情况下会将其安装为命令行工具,让人头疼的是,必须始终要记得去运行这一工具。

幸运的是,这正是生成工具的用处所在。返回到 Gulpfile,让我们使用 Gulp 在每个源文件(现在只有一个)上运行 JSHint,方法是告知它相关源文件,然后要求它对每个源文件执行 JSHint:

// Don't need it in this file, but we need it installed
require('jshint');
require('jshint-stylish');
const gulp = require('gulp');
const jshint = require('gulp-jshint');
gulp.task('jshint', function() {
  return gulp.src('*.js')
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

运行时,工具将指出存在对当前目录中的所有“js”文件(包括 Gulpfile 本身)的一些建议的更改。哈。你真的不在乎检查 Gulpfile,因此让我们从要运行的文件集合中将其删除:

gulp.task('jshint', function() {
  return gulp.src(['*.js', '!gulpfile.js'])
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

在传递到管道中的下一阶段之前,这将从当前文件列表中删除“gulpfile.js”。事实上,你可能想要将大多数代码放在“src”目录(或“server”和“client”目录,对于每一方),从而将它们及其任何子目录添加到要处理的文件列表:

gulp.task('jshint', function() {
  return gulp.src(['*.js', '!gulpfile.js', 'server/**/*.js', 'client/**/*.js'])
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

每个路径中的“double-star”将执行递归行为以选取每个子目录中的“js”文件。

从表面上来看,这真是太棒了,但它真的没有为你执行太多操作: 每当想要查看需要修复的内容时,你仍需手动键入“gulp jshint”(或者,如果你将其作为依赖项绑定到“default”任务,则只需键入“gulp”)。为何不能在代码更改时随时运行,就像 IDE 一样?

当然可以这样做,如图 1 中所示。

图 1 使用 Gulp 实现连续自动化

// Don't need it in this file, but you need it installed
require('jshint');
const gulp = require('gulp');
const jshint = require('gulp-jshint');
gulp.task('default', ['watch']);
gulp.task('watch', function() {
  gulp.watch(['*.js', '!gulpfile.js', 'client/**/*.js', 'server/**/*.js'],
    ['jshint']);
});
gulp.task('jshint', function() {
  return gulp.src(['*.js', '!gulpfile.js', 'client/**/*.js', 'server/**/*.js'])
    .pipe(jshint())
    .pipe(jshint.reporter('default'));
});

现在,在运行“gulp”时,命令行只会暂停并等待。现在 Gulp 处于“watch”模式,在这种情况下,它将密切注视传递给“gulp.watch”调用的任何文件,如果任何这些文件发生更改(即保存文件—很遗憾 Gulp 无法观测文本编辑器内部),它将立即在文件的完整集上运行“jshint”任务。持续监视。

更加充分地使用 Gulp

了解 Gulp 任务处理文件方式的关键之一在于探究对管道的调用。Gulp 从“流”(而非任务或文件)的角度进行处理。例如,一个简单的 Gulp 任务是将文件从“src”目录复制到“dest”目录,如下所示:

gulp.task('copy-files', function() {
  gulp.src('source/folder/**')
    .pipe( gulp.dest('dest/folder/**') );
});

实质上,gulp.src 会拾取每个文件并按原样存入 gulp.dest 指定的目标。只需一步便可在管道中完成需要对这些文件执行的任何操作,在管道中继续进行下一步操作之前,每个文件会流经该管道。这是“管道和筛选器”的 Unix 架构样式,以令人难以置信的精练方式引入到 Node.js 生态系统。Windows PowerShell 基于同种体系结构构建而成,适用于之前已在 .NET 世界中了解类似内容的人员。

因此,如果你希望 Gulp 仅通过管道处理每个文件,可使用一个相应的插件,即 gulp-filelogger,该插件会将自己处理的每个文件打印到控制台上:

gulp.task('copy-files', function () {
  gulp.src(srcFiles)
    .pipe(filelogger())
    .pipe(gulp.dest(destDir));
});

结果如下所示:

Teds-MacBook-Pro:gulpdemo tedneward$ gulp copy-files
[20:14:01] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[20:14:01] Starting 'copy-files'...
[20:14:01] Finished 'copy-files' after 14 ms
[20:14:01] [/Users/tedneward/Projects/code/gulpdemo/index.js]
Teds-MacBook-Pro:gulpdemo tedneward$

注意 Gulp 报告完成后显示输出的方式。Gulp 确实在大部分时间内可以异步处理这些流,以缩短“生成”时间。大部分时间内,开发者既不知道也不关心并行执行的操作,但对于按精确顺序执行操作的那些时间来说,这是非常重要的,一点也不奇怪,Gulp 插件社区有一些插件可串行化执行并确保一切操作按顺序执行。Gulp 4.0 将添加两个新函数(parallel 和 serial)以使其更加明确,但因为尚未发布,所以你还需要等待一段时间。

顺便提一下,Gulp 本身仅包含目前你所看到的四个函数:gulp.task、gulp.watch、gulp.src 和 gulp.dest。其他还包括所有插件、npm 模块或手动编写。这使 Gulp 本身极易理解。实际上,对于按字付费的文章编写者而言,这并不容易。

一次性全面了解 Gulp

Gulp 本身并不是一个复杂的工具,但对于此性质的任何工具,它的真正优势在于相关社区中涌现的大量插件和补充工具。完整列表可从 gulpjs.com/plugins 获取,但图 2 展示了 Gulp 方法的代表性示例,介绍了如何自动将项目发布到 GitHub,包括需要掌握的 Git 命令。

图 2 Gulp 方法

var gulp = require('gulp');
var runSequence = require('run-sequence');
var conventionalChangelog = require('gulp-conventional-changelog');
var conventionalGithubReleaser = require('conventional-github-releaser');
var bump = require('gulp-bump');
var gutil = require('gulp-util');
var git = require('gulp-git');
var fs = require('fs');
gulp.task('changelog', function () {
  return gulp.src('CHANGELOG.md', {
    buffer: false
  })
    .pipe(conventionalChangelog({
      preset: 'angular' // Or to any other commit message convention you use.
    }))
    .pipe(gulp.dest('./'));
});
gulp.task('github-release', function(done) {
  conventionalGithubReleaser({
    type: "oauth",
    token: '' // Change this to your own GitHub token.
  }, {
    preset: 'angular' // Or to any other commit message convention you use.
  }, done);
});
gulp.task('bump-version', function () {
// Hardcode the version change type to "patch," but it might be a good
// idea to use minimist (bit.ly/2cyPhfa) to determine with a command
// argument whether you're doing a "major," "minor" or a "patch" change.
  return gulp.src(['./bower.json', './package.json'])
    .pipe(bump({type: "patch"}).on('error', gutil.log))
    .pipe(gulp.dest('./'));
});
gulp.task('commit-changes', function () {
  return gulp.src('.')
    .pipe(git.add())
    .pipe(git.commit('[Prerelease] Bumped version number'));
});
gulp.task('push-changes', function (cb) {
  git.push('origin', 'master', cb);
});
gulp.task('create-new-tag', function (cb) {
  var version = getPackageJsonVersion();
  git.tag(version, 'Created Tag for version: ' + version, function (error) {
    if (error) {
      return cb(error);
    }
    git.push('origin', 'master', {args: '--tags'}, cb);
  });
  function getPackageJsonVersion () {
    // Parse the json file instead of using require because require caches
    // multiple calls so the version number won't be updated.
    return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
  };
});
gulp.task('release', function (callback) {
  runSequence(
    'bump-version',
    'changelog',
    'commit-changes',
    'push-changes',
    'create-new-tag',
    'github-release',
    function (error) {
      if (error) {
        console.log(error.message);
      } else {
        console.log('RELEASE FINISHED SUCCESSFULLY');
      }
      callback(error);
    });
});

此示例介绍了许多内容:如何按特定顺序运行任务,使用 Gulp 插件来生成约定变更集文件,执行 GitHub 样式发布消息,提高语义版本以及更多内容。一切功能都在 gulp 版本中提供;功能非常强大。

总结

本文并不包含大量代码,你只需重启整个应用程序即可获取许多功能,并基本上将应用程序提升到过去一年左右时间构建内容的同一级别(和更高级别)。你一定喜欢搭建基架!

更加重要的是,在运行基架搭建程序之前手动一点一点地构建所有部件,可以更加轻松地理解总体代码以及每个部分所执行的操作。例如,打开 routes.js 将类似于先前手动构建的路由表,package.json(位于项目的根目录)将变大,但仍与你所使用的保持相同。

实际上,除了使用 Yeoman 本身之外,唯一的新内容是介绍了一个生成工具以将所有相关部件收集到正确的位置,这是我下次要介绍的内容。在那以前,祝你编码愉快!


Ted Neward 是本部位于西雅图的 Polytechnology 公司的顾问、讲师和导师。他是一位 F #MVP,写过 100 多篇文章,独自撰写并与人合著过十几本书。如果你有兴趣请他参与你的团队工作,请通过 ted@tedneward.com 与他联系,或通过 blogs.tedneward.com 访问其博客。

衷心感谢以下技术专家对本文的审阅: Shawn Wildermuth