Yeoman 如何改变了我们的工作方式

Avatar of Noam Elboim
Noam Elboim

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 的免费积分!

以下是来自 Noam Elboim 的客座文章,他是 myheritage.com 的开发人员。Noam 深入研究了 Yeoman——从一无所知或不了解它如何在工作中提供帮助,到为他们构建定制生成器。如果您认为可以帮助您根据自己的喜好构建新项目的工具可能对您有所帮助,请查看 Noam 的旅程!

在我加入 MyHeritage 的第一天,我记得与网页开发团队领导进行了以下交流

团队领导: 您有使用 Yeoman 等 Node.js 模块的经验,对吗?

我: 是的,我知道一些。

团队领导: 我认为在这里使用 Yeoman 可以节省我们创建新页面的大量时间。您认为您可以处理类似的事情吗?

好吧,那是我第一天上班;我几乎不知道我的座位在哪里。但是,我清楚地记得我在想什么,我不明白 Yeoman 如何在这里提供帮助。

对我来说,以及对许多其他人来说,Yeoman 是一种工具,用于在不超过两秒钟的时间内从零到英雄生成一个完整的可工作应用程序。它不适用于向现有应用程序添加部分。

我完全错了。请允许我与您分享我们如何在 MyHeritage 创建了一个 Node.js 模块,它扩展了 Yeoman 的功能,从而在日常任务中节省了大量时间。

问题

在 MyHeritage 中创建新页面曾经是一项艰巨的任务。每个新创建的页面都需要大约两天的工作才能准备好所有内容,包括资产管理、服务器端组件和应用程序的客户端引导。简而言之:大量文件。

大多数开发人员不会定期创建新页面。在真正的工作开始之前,需要花费大量时间才能了解如何创建新页面。然后,一旦页面创建完成并且工作“完成”,仍然需要花费数小时的时间来调试任何无法正常工作的内容。

两天对于像这样的简单任务来说,投入的时间太多了。

一线希望

在我与团队领导交谈后的几个月里,创建新页面的负担越来越重,我们无法继续走这条路。我们知道需要立即找到解决方案,并且它必须简单、易于维护,最重要的是,使用起来非常快。

首先,我们分解了与创建新页面相关的难题。该过程需要很多步骤,包括路由配置和翻译服务,运行终端命令以创建特定文件(如 A/B 测试或 Sass 文件),甚至运行一些 Gulp 任务(如编译 Sass 或创建精灵图)。

在这一点上,我们确信扩展 Yeoman 对我们来说是最好的选择,但我们仍然需要弄清楚它如何在保持其核心简单性和易用性的同时满足所有这些需求。

关于 Yeoman 的一些话

Yeoman 帮助您启动新项目,规定最佳实践和工具,帮助您保持高效。

这是 Yeoman 网站上的第一行,而且完全正确。

它是一个 Node.js 模块,拥有数千个生成器可供选择。所有生成器的工作原理基本相同——首先,用户会在基于 Inquirer.js 的用户界面中被问到一系列问题。然后,使用用户的输入,Yeoman 会生成正确的新文件。

一个用于提供项目名称的字符串输入提示。
一个复选框提示用于设置“主页面”配置。

为什么选择 Yeoman

我们有各种选择,从使用完整的可工作生成器到从头开始构建。我们选择 Yeoman 有几个原因

首先是简单性。Yeoman 具有清晰的 API、用于入门的生成器生成器,以及作为 Node.JS 模块的极简易用性。

其次是可维护性。Yeoman 被全球数千人使用,它基于 Backbone.js 模型,允许以易于理解的方式进行扩展。

最后是速度。简而言之:Yeoman 具有速度优势。

我们的解决方案

我们想在 Yeoman 流程的两半之间添加另一个阶段,以扩展我们通过一组有序函数从用户那里收集的数据。例如,我们可能想要能够从 CamelCase 获取 snake_case,或捕获在终端中执行的命令的输出。我们要求此阶段可通过 JSON 文件进行配置。

我们将每个函数分解为一个对象,其中包含要执行的函数的名称、参数以及存储输出的位置。我们使用 Node.JS,以便这些函数异步运行并返回 promise。当所有 promise 解析后,我们将生成文件。以下是如何 Generator 类使用名为“generators”的 Yeoman-generator 模块的示例

var generators = require('yeoman-generator'),
    services, prompts, actions, preActions, mainActions,
    _this;
  
module.exports = Generator;

/**
 * Generator object for inheritance of Yeoman.Base with different services, prompts and actions files.
 * To use the generator in you Yeoman generator, create a new Generator instance with your local files and export the return value of Generator.getInstance()
 * @param externalService - local generator services.js file
 * @param externalPrompts - local generator prompts.json file
 * @param externalActions - local generator actions.json file
 * @constructor
 */
function Generator (externalService, externalPrompts, externalActions) {
  services = externalService;
  prompts = externalPrompts;
  actions = externalActions;
  preActions  = actions.pre;
  mainActions = actions.main;
}

/**
 * Get instance will create extension to the Yeoman Base with your local dependencies
 * @returns {Object} Yeoman Base extended object
 */
Generator.prototype.getInstance = function () {
  return generators.Base.extend({
    initializing: function () {
      _this = this;
      this.conflicter.force = true; // don't prompt when overriding files
    }, 
    prompting: function () { /* ... */  },
    writing:  function () { /* ... */  },
    end:  function () { /* ... */  }
  });
};

如何构建生成器

一旦基础准备就绪,创建新生成器就轻而易举了。我们只需要创建一个包含 4 个文件和一个模板文件夹的新文件夹。文件夹的名称就是生成器的名称。这 4 个文件是

  1. 提示——一个充满对象的 JSON 文件,它将提示的问题映射到存储的数据。
    [
      {
        "type": "input",
        "name": "name",
        "message": "Name your generator:",
        "default": "newGenerator"
      },
      {
        "type": "list",
        "name": "commonServices",
        "message": "Want some common services:",
        "choices": ["yes", "no"]
      }
    ]

    在此示例中,第一项将要求用户“命名您的生成器:”,默认答案为“newGenerator”。它将答案存储在 data 对象中的“name”字段中。

    第二项询问一个多选问题,选项是“yes”和“no”。默认选项是第一个选项“yes”。

  2. 服务——一个 JS 文件,其中包含我们扩展用户数据所需的所有函数。
    var exec  = require('child_process').exec,
      chalk = require('chalk'),
      Q     = require('q');
    
    module.exports.generateSprite = generateSprite;
    
    /**
     * Run gulp sprites in our new sprite folder
     * @param {String} name - the name of the project
     * @return {Object | Promise}
     */
    function generateSprite (name) {
      return Q.Promise(function (resolve, reject) {
        console.info(chalk.green("Compiling new sprite files..."));
        if (name) {
          exec('gulp sprites --folder ' + name, function () {
            resolve();
          });
        }
        else reject("Missing name in generateSprite");
      });
    }

    此示例解释了一个名为“generateSprite”的服务,它将运行一个为项目预先配置的终端命令。此特定脚本将创建一个精灵图文件夹。如果该函数成功运行该命令,它将解析一个 promise,该 promise 将标记该操作为成功。可以根据结果生成模板。

  3. 操作——一个 JSON 文件,它映射了我们在生成文件之前需要执行的所有函数(“pre”部分)以及另一个模板文件与其最终生成位置的映射,即“main”部分。
    {
      "pre": [
        {
          "dependencies": ["name"],
          "arguments": ["name"],
          "output": "spriteFolder",
          "action": "generateSprite"
        }
      ],
      "main": [
        {
          "dependencies": ["name"],
          "optionalDependencies": ["spriteFolder"],
          "templatePath": "output_file.js",
          "destinationPath": "./sprites/<%= name %>output_file.js"
        }
      ]
    }

    在“pre”部分中,有一个任务,只有在“name”字段已定义或为 true 时才会运行(基于“dependencies”数组中定义的字段要求)。要提供给服务函数的参数位于“args”对象中的“arguments”数组中。函数的输出保存在“output”字段指定的位置。函数的名称由“action”字段定义。在此实现中,您只能提供一个函数,但可以通过在“action”中使用数组轻松地支持多个函数。

    在“main”部分中,有一个具有类似功能的任务。如果“dependencies”中的字段丢失或有故障,则不会生成文件。但是,对于“optionalDependencies”来说情况并非如此,因为它们实际上是可选的。暴露给模板引擎的字段在“dependencies”和“optionalDependencies”中定义。

    “templatePath”具有模板文件的路径,而“destinationPath”是文件的目标位置。“destinationPath”可以使用“pre”部分中的数据,如我们在这里看到的“name”字段(<%= name %>)。

    请注意,我们使用 EJS 模板语言。Yeoman 实际上支持除了 EJS 之外的其他模板引擎,如 handlebars。

  4. 索引——一个 JS 文件。Yeoman 要求每个生成器都拥有一个索引文件。索引链接了其他 3 个文件并继承自基类。基本上,它使用分离的提示、服务和操作创建一个基类的实例。
    var Generator   = require('../generator'),
        services    = require('./services'),
        prompts     = require('./prompts'),
        actions     = require('./actions');
        
    var generator = new Generator(services, prompts, actions);
        
    module.exports = generator.getInstance();

    如您在此示例中所见,它创建了一个实例并将其导出以与 Yeoman 协同工作。

  5. 模板 - 就像字面意思一样:只是用来创建文件的模板。
    <h1>My first template in <%= name %>!</h1>
    <% if (spriteFolder) { %>
      I even managed to create sprites for it!
    <% } %>

    请注意,扩展数据和创建模板之间的平衡完全取决于您想要创建的生成器的类型。例如,一个只创建新文件而没有复杂数据的生成器可能不需要扩展。相反,我们可能想要一个只运行一组系统命令的生成器,它将没有模板。

    这两种情况对于某些用途来说都是可能的,甚至很可能。能够同时做这两件事,或者只做其中一件,体现了使用和创建满足任何需求的生成器的灵活程度。

我们获得了什么

由于我们通过创建用于新页面的生成器解决了问题,我们已经能够使用 Yeoman 创建更多生成器,包括一个用于在我们的后端 API 中创建新部分的生成器,以及另一个用于创建完整的 Angular 应用程序(带单元测试等)的生成器。

随着我们每天越来越多地使用 Yeoman,对我们来说,优点是显而易见的,但我们也注意到了一些缺点,您应该牢记这些缺点。

优点

  • 快速 - 创建一个新页面现在大约需要 1 分钟。
  • 最佳实践和约定 - Yeoman 强制开发人员使用最佳实践,并有助于确保我们的团队遵循和学习约定。团队可以确保制作的每个页面都使用相同的约定,无论由哪个开发人员创建。您是否想确保每个页面都使用一些新的闪亮的服务?只需将其添加到“新建页面”生成器中,您就完成了。
  • 更清洁的代码环境 - 由于我们取消了开发人员复制/粘贴他们不完全理解的代码部分的能力,因此 bug 出现的频率往往要低得多,不必要的代码重复问题也小了很多。

缺点

  • 维护 - 这是最难的部分。最佳实践往往会不断发展和变化。您必须始终确保生成器处于代码库的前沿,这需要投资。它不是一个“一击即中”的项目 - 您必须随着代码库和技术的演变而更新它。
  • 适合所有用途 - 这更像是一个挑战而不是一个缺点,但如果生成器没有正确实现,可能会导致开发人员不使用您辛辛苦苦构建的生成器。重要的是,生成器要通用,但仍然可以为开发人员节省大量在重复性且令人沮丧的任务上花费的时间。例如,与其实现一个函数,不如添加一个 TODO,并附带一些说明。

结论

我们的生成器非常特定于我们的项目,因此这些生成器不是开源的,但有很多更广泛有用的生成器可以使用或作为您构建自己的生成器时参考,例如 这个用于 Angular 2 的生成器

在我们为许多重复性任务创建了生成器之后,使用 Yeoman 确实让我们的日常工作变得更轻松、更有效率。我们甚至制作了一个生成器生成器!但它也为我们的生活带来了无法忽视的意外挑战。在应该自动化的内容和需要由开发人员手动完成的内容之间存在平衡,这是为每个项目和需求确定这一平衡的问题……但这将是另一个时间讨论的话题。