基于yeoman定制的交互式命令行脚手架

1,555 阅读8分钟

一、前言

随着开发团队不断发展壮大,在人员增加的同时也带来了协作成本的增加;业务项目越来越多,类型也各不相同。常见的类型有基础组件、业务组件、基于React的业务项目、基于Vue的业务项目等等。如果想要对每个项目进行一些规范上的约束比如Git提交规范、Javascript规范简直难于登天。所有的这些,只是因为还欠缺一个好用的工程化工具,在项目创建的初期自动的将这些目录结构和文件生成、并且集成工程常见的规范来进行约束。

二、问题分析以及解决思路

2.1 谈谈目前团队的痛点

l 业务部门从git上拉下模板代码后,还要根据具体的业务需要进行配置,修改代码对于前端框架有一定的学习的成本,比较麻烦,对于业务来讲也没有必要学习这些框架知识

l 修改配置代码的工作基本上是在项目开始就要完成的,是一次性的,后续不用关心有什么变化

l 对于页面生成,路由生成这些操作,基本上是固定的操作,每次新建js文件,并在旧的文件中添加固定格式的代码,既繁琐又有规律可循,所以考虑是不是可以一个命令就能够添加一个页面文件,并生成路由呢

基于上面几点,考虑做一个前端脚手架项目,通过脚手架来复用项目结构,并把对于项目结构的配置工作和复杂枯燥的操作都打包到这个脚手架的功能上,通过命令行就可以完成以前好几步才能完成的步骤,方便业务开发并提升研发效率。

好了,说干就干,在网上搜索了一下,发现Yeoman就是干这个事情的,而且是谷歌开发的脚手架工具,尝试了一下非常好,下面就记录一下yeoman的设计思路和使用过程。

2.2 基于Yeoman generator的设计思路

我们需要给每个工程类型的项目创建一个generator。按照目前前端技术栈的发展情况来看,一个团队一般会有3~5个generator。把这些generator看成一个个的插件,通过工具上层的CLI命令来暴露给开发者使用。

在generator之下,需要开发一系列服务和集成规范。包括和Git仓库打通,也就是通过脚手架初始化目录时,先对开发者鉴权。之后根据开发者输入的项目名称在远程Git仓库里面创建仓库并且授予开发者权限。后期功能完善之后,可以做一些锦上添花的工作,比如进行数据统计,分析各个业务仓库使用的generator版本信息,是否集成了最新的feature等等。

三、实现定制的generator

3.1 账号准备

npmjs 账号,用于publish到npm。

Github 账号,npm中显示你的源码,同时方便Yeoman官网中能搜到你的generator。正如其描述的:

Your generator must have a GitHub repository description, the yeoman-generator keyword and a description in package.json to be listed.

3.2 下载安装yo和generator-generator

在已经有装好node和npm的前提下,需要全局安装yo和generator-generator

npm install -g yo

之后运行generator-generator来创建我们自己需要的generator的基础框架

npm install -g generator-generator

3.3 开始行动

3.3.1 创建一个脚手架的基础框架

运行generator-generator来创建我们的脚手架基础框架,运行如下命令:
yo generator

接下来会有一系列的询问问题,其中generator name需要设置为必须以generator-为前缀,因为generator都是普通全局安装的Node.js模块,所以Yeoman完全依赖于文件系统找到它们。

基础框架安装完成之后的目录结构如下图:



我们对于脚手架的定制开发工作主要在下面两个文件夹

l generators/app/templates 目录
放置脚手架代码模板的文件夹

l generators/app/index.js 文件
配置用户输入信息,模板迁移和替换规则,安装项目依赖模块;该文件是Generator的子类,重点完成三个方法的定制,分别是prompting,writing,install,下面会重点讲解。

3.3.2 配置用户输入项

脚手架的工作方式是:先询问用户的配置需求,比如你的项目名字是什么?要使用哪些工具类?然后根据用户的输入,完成项目文件的初始化工作。

我们先看下用户输入配置的代码,地址:generators/app/index.js

  prompting() {    let prompts = [];    // 如果没有指定appname则提示用户输入    if (!this.options.appname) {      prompts.unshift({        type: 'input',        name: 'appname',        message: "Input your project's name",        default: DEFAULT_APPNAME,        validate: name => {          return !/\s+/.test(name.trim());        }      });    }
    prompts = prompts.concat(require('./_prompts'));
    return this.prompt(prompts).then(res => {      // 后续可通过this.props.xxx使用props;      let appname = res.appname || this.options.appname;      let options = Object.assign({}, res, { appname });      // 模块依赖      this.renderOpts = options;    });  }

prompting方法主要是来完成和用户交互的,交互的用户输入信息都放在prompts数组中:

· name 用户输入项的标识,在获取用户输入值的时候会用到

· message 是给用户的提示信息

· type 非必填,默认是text,即让用户输入文本;confirm是选择输入“Yes/No"

· default 非必填,用户输入的默认值

用户输入详细解读请参考inquirer类库,此处不再展开www.npmjs.com/package/inq…

3.3.3 模板文件

上面已经提到generators/app/templates目录存储的是文件的模板文件,是生成新的项目结构的原材料;

一个新的项目的文件和结构,是由三部分组成

· 固定文件 直接从template目录copy到项目目录即可

· 加工文件 根据上一步用户的输入,对templates目录下的模板进行二次加工,再copy到用户指定的目录中,以完成项目的初始化

· 可选文件 根据用户的选择,如果需要则copy,不需要则不copy

对于加工文件,在templates文件中可以使用EJS模板语法,把模板和用户的输入信息结合起来,就生成了一个新的项目文件,EJS的模板语法如下:

· 赋值 <%= appName%>

· 表达式 <% if(someAnswer){ xxx } %>

3.3.4 加工模板

项目文件的模板拷贝和用户输入替换工作都在writing方法中实现,示例代码如下:

 writing() {    const DestFolder = this.options.current      ? ''      : Path.join(this.renderOpts.appname, '/');    // 添加隐藏文件 .文件名称在linux下会有问题,所以.xxx在template里改为_xxx    let targets = [      // 不需要加工的文件      ['mock/**/*', 'mock', false],      ['src/**/*', 'src', false],      ['README.md', 'README.md', false],      ['_editorconfig', '.editorconfig', false],      ['_env', '.env', false],      ['_eslintrc', '.eslintrc', false],      ['_gitignore', '.gitignore', false],      ['_prettierignore', '.prettierignore', false],      ['_prettierrc', '.prettierrc', false],      // 需要加工的文件      ['package.ejs', 'package.json', true],      ['umirc.ejs', 'umirc.js', true]    ];    // 遍历文件数组,处理并移动文件到目标文件夹下    _.forEach(targets, file => {      let fromFile = '';      let toFile = '';      let opts = {};      fromFile = file[0];      toFile = file[1];      opts = file[2] ? this.renderOpts : opts;      this.fs.copyTpl(        this.templatePath(fromFile),        this.destinationPath(DestFolder + toFile),        opts,        {},        { globOptions: { dot: true } }      );    });  }

需要重点关注四个方法:

· this.templatePath返回template目录下文件的地址

· this.destinationPath指定加工完成后文件的存放地址,一般是项目目录

· this.fs.copy把文件从一个目录复制到另一个目录,一般是从template目录复制到你所指定的项目目录,用于固定文件可选文件(根据用户选择)

· this.fs.copyTpl和上面的函数作用一样,不过会事先经过模板引擎的处理,一般用来根据用户输入处理加工文件

那么怎么加工文件呢?上面我们已经介绍了EJS模板的赋值和表达式语法,比如我们上面有一个输入项是appName

const prompts = [
 {
   name: 'appName',
   message: 'your appName name?'
 }];

我们把这个输入项作为html的title,那么我们就可以在template文件夹里面放入下面的模板文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title><%= appName %></title>
  </head>
  <body>
    <div id="vue"></div>
  </body>
</html>
3.3.5 安装依赖

好了,项目文件加工完成并复制到了指定的目录,接下来要运行我们的项目,还需要使用npm install 安装项目的依赖,那么可不可以把这个操作也放到脚手架的安装过程中呢?下面的 install() 方法就可以:

install() { 
 // 安装npm依赖和bower依赖 
 //this.installDependencies();   
 // 只安装bower依赖  
 //this.bowerInstall();  
 // 只安装npm组件  this.npmInstall(); 
}

其中有三个方法可以使用,既可以安装npm依赖包,也可以安装bower依赖包,具体请参考上面的代码注释。

3.3.6 本地运行脚手架

综上,一个脚手架最基本的功能就完成了,首先先将这个脚手架链接到本地:
npm link // 如果提示权限问题请使用sudo npm link

此时脚手架已经可以本地使用了,在本地创建一个项目目录,进入该目录,尝试使用该脚手架,比如你的脚手架项目名称为generator-name,命令行中应该去掉脚手架项目的前缀“generator-”来运行:
yo name

根据设定的提示和输入信息,Yeoman会一步一步安装你的项目文件,最终生成你指定的项目结构。

3.3.7 发布脚手架到npm

如果你希望自己的脚手架给更多的人提供方便,可以把它发布到npm上。

首先需要一个npm账号,如果没有可以使用npm adduser创建;
如果有则运行npm login登陆,然后到工程根目录下,运行npm publish就可以发布了。

结语

好了,关于Yeoman的用法就介绍到这了,关于yeoman生命周期以及高级功能,可参考Yeoman官网。其实Yeoman生成脚手架不只限于前端,任何语言的脚手架都可以用Yeoman来实现,具体可参考官网。