15

背景

前几天看了一篇文章大受启发:我理想中的前端工作流,现在工作中一直在使用gulp和webpack做自动化,在单独项目中效果很不错。但是随着项目逐渐怎多,是需要使用一个工具来帮你快速创建和规范项目。

Yeoman是什么

Yeoman是一个脚手架,可以快速生成一个项目的骨架。官网上有很多大家已经写好的脚手架,也可以自己写一个适合自己的,接下来我会翻译下官网的教程,学习的同时把经验分享给大家。

Yeoman能做什么

Yeoman只是帮我们生成项目的骨架是远远不够的,官网上介绍,Yeoman是由三部分组成的:脚手架工具 - Yo、构建工具 - Grunt or Gulp、包管理工具 - Bower or npm。

创建Yeoman

翻译yeoman官网Creating a generator流程

(一)Getting started 快速开始
(二)Running Context 生命周期
(三)User Interactions 和使用者互动
(四)composability 组合
(五)Managing Dependencies 依赖管理
(六)Interacting with the file system 文件操作


(一)快速开始

原文地址:http://yeoman.io/authoring/index.html

1.1 建立一个node模块

1.首先要建立一个文件夹,在这个文件夹内写你的generator,这个文件夹的名字 必须 被命名为generator-name,name为你generator的名字,假如我想写一个vue的脚手架,我可以命名为:generator-vue,这个很关键,Yeoman文件系统只会信任这种规范的generator。

mkdir generator-vue

2.建立node模块,首先要必备文件 package.json ,这个文件可以通过执行 npm init 指令来生成,前提是需要安装 node 及 npm。

{
  "name": "generator-vue",
  "version": "0.1.0",
  "description": "",
  "files": [
    "app",
    "router"
  ],
  "keywords": ["yeoman-generator"],
  "dependencies": {
    "yeoman-generator": "^0.20.2"
  }
}

几点要求:

1. name:必须格式为 `generator-你项目的名字`。
2. keywords:数组中必须有 `yeoman-generator`,这样你的项目才会被[Yeoman官方的generators列表](http://yeoman.io/generators/)所收录。 
3. 如果需要添加其他的属性可以去[npm官网文档](https://docs.npmjs.com/files/package.json#files)中查看。
1.2 文档结构

通过1.1步骤已经有了package.json,下一步在新建两个文件夹分别叫app和router,结构如下。

├───package.json
├───app/
│   └───index.js
└───router/
    └───index.js
1.2.1 默认项目

当你执行Yeoman指令 yo vue(上面你已经建立的项目名字)的时候,他会默认执行你根目录下app/index.js的内容,所以一个新项目,app/ 目录是必须的。

1.2.2 子项目

router/作为子项目,可以通过 yo vue:router 来执行。

1.2.3 更改文件目录
├───package.json
└───generators/
    ├───app/
    │   └───index.js
    └───router/
        └───index.js

如果不喜欢把所有项目都放在根目录下,Yeoman还允许把项目放在 generators/ 下面,改写上面的例子:

├───package.json
└───generators/
    ├───app/
    │   └───index.js
    └───router/
        └───index.js

如果更改了文件目录,要同步修改 package.json 中对应文件的目录结构:

{
  "files": [
    "generators/app",
    "generators/router"
  ]
}
1.3 扩展generator
1.3.1 重写构造函数
module.exports = generators.Base.extend({
  // The name `constructor` is important here
  constructor: function () {
    // Calling the super constructor is important so our generator is correctly set up
    generators.Base.apply(this, arguments);

    // Next, add your custom code
    this.option('coffee'); // This method adds support for a `--coffee` flag
  }
});
1.3.2 添加自己的方法
module.exports = generators.Base.extend({
  method1: function () {
    console.log('method 1 just ran');
  },
  method2: function () {
    console.log('method 2 just ran');
  }
});

下一步的时候当你运行generator的时候,会看到这两句console输出在控制台。

1.4 运行generator
1.4.1 安装全局generator

在项目跟路径下 generator-name(vue)/ 执行指令:

npm link

过程中,将会安装node模块依赖,和创建软连接指向你当前项目。

//1.到本地全局node模块路径下
cd /usr/local/lib/node_modules 

//2.查看列表
ll 

//3.会看到已经安装了一个全局的geneator-vue模块
npm
geneator-vue -> /Users/lvjinlong/generator-vue
gulp
..  

//4.此时在任意新建的项目文件夹中yo项目的名字,会看到上面实例中的console打印出来的结果。
yo vue
1.4.2 寻找根目录

安装geneator的时候,Yeoman会搜索你的文件夹,会把包含 .yo-rc.json 文件的文件夹作为你的根目录来初始化项目。

问:那么,.yo-rc.json 是个什么东西呢?
答:当你 第一次 调用 this.config.save() 的时候,系统会生成这个文件。

问:那么,this.config.save() 这个方法的作用是什么呢?
答:官网 这篇文章会有讲解,大体意思是会利用 .yo-rc.json 来存储或是读取用户相关信息。

所以当你初始化一个项目的时候,别忘记清除掉之前系统生成的.yo-rc.json


(二)生命周期

原文地址:http://yeoman.io/authoring/running-context.html

2.1 Prototype methods as actions

每个方法会直接附加在generator原型上作为一个action,每个action按照一定的循序执行在Yeoman的生命周期中。

这些方法相当于直接执行了 Object.getPrototypeOf(generator)
所有方法都会 自动执行 。如果不想都自动执行,请往下看。

2.1.1 私有方法

只有私有方法在Yeoman中才不会自动执行,下面有三种办法帮你创建一个私有方法。

1. 在方法名前面加下划线(例如:_method
2. 使用实例方法
generators.Base.extend({
  init: function () {
    this.helperMethod = function () {
      console.log('won\'t be called automatically');
    };
  };
});
3.继承父generator
var MyBase = generators.Base.extend({
    helper: function () {
      console.log('won\'t be called automatically');
    }
  });

  module.exports = MyBase.extend({
    exec: function () {
      this.helper();
    }
  });
2.2 生命周期

Yeoman中的定义了生命周期钩子,和这些钩子命名相同的会按照顺序执行,如果和这些钩子名称不一样则默认为 default

这些生命周期钩子 按顺 序为:

  1. initializing:初始化方法(检验当前项目状态、获取configs、等)
  2. prompting:获取用户选项
  3. configuring:保存配置(创建 .editorconfig 文件)
  4. default:如果函数名称如生命周期钩子不一样,则会被放进这个组
  5. writing:写generator特殊的文件(路由、控制器、等)
  6. conflicts:冲突后处理办法
  7. install:正在安装(npm、bower)
  8. end:安装结束、清除文件、设置good bye文案、等

(三)和用户互动

原文地址:http://yeoman.io/authoring/user-interactions.html

Yeoman默认在终端中执行,但是也支持在多种不同工具中执行。这时候我们使用 console.log() 或是 process.stdout.write() 用户就可能看不到,Yeoman中使用 generator.log() 来统一打印输出结果。

3.1和用户互动
3.1.1 Prompts - 提示框

提示框是Yeoman主要和用户交流的手段,是通过Inquirer模块来实现的,所有的API及参数可以看这里,执行以下实例看下效果:

module.exports = generators.Base.extend({
  prompting: function () {
    var done = this.async();
    this.prompt({
      type    : 'input',
      name    : 'name',
      message : 'Your project name',
      default : this.appname // Default to current folder name
    }, function (answers) {
      this.log(answers.name);
      done();
    }.bind(this));
  }
})
3.1.2 Remembering user preferences 记录用户预设参数

一个确定的答案,比如帐号,用户可能多次提交同一个答案,这时候可以用Yeoman提供 store 来存储这些答案。

this.prompt({
  type    : 'input',
  name    : 'username',
  message : 'What\'s your Github username',
  store   : true
}, callback);

这时候会在跟路径下生成一个.yo-rc.json文件,里面会存储name信息。可以参考官网storage这一节

3.1.3 Arguments - 参数

参数直接通过命令行传递,例如:

yo webapp my-project

这个例子中,my-project 是第一个参数。

通知系统我们需要参数,我们使用 generator.argument() 方法,这个方法接受两种形式:

  1. name(String) -- generator['name']
  2. hash(key-value) -- 哈希值的形式,接受以下参数作为key值

    • desc -> 参数描述
    • required -> 是否为必须传递 [ ture | false ]
    • optional -> 是否可选 [ ture | false ]
    • type -> 参数类型 [ String | Number | Array | Object]
var _ = require('lodash'); //需要提前安装lodash模块,提供一些常用方法
module.exports = generators.Base.extend({
  //注: arguments和options必须在constructor中定义.
  constructor: function () {
    generators.Base.apply(this, arguments);
    //appname为一个必须的参数
    this.argument('appname', { type: String, required: true });
    //用驼峰式把这个参数保存起来
    this.appname = _.camelCase(this.appname);
  }
});
3.1.4 Options - 选项

Options(选项)看起来像是Arguments(参数),但是他们是在命令行中的标志。

实例:举一个官网团队的脚手架demo - webapp - 15行

module.exports = generators.Base.extend({
  constructor: function () {
    generators.Base.apply(this, arguments);
    this.option('skip-welcome-message', {
      desc: 'Skips the welcome message',
      type: Boolean
    });
  }
})

用法:
webapp - options

yo webapp --skip-install
3.2 输出信息

输出信息使用 generator.log 模块,和js的 console.log() 基本一致。

module.exports = generators.Base.extend({
  myAction: function () {
    this.log('Something has gone wrong!');
  }
});

传值的方式同Arguments(参数),字符串或hash。区别是参数:

* desc:描述
* alias:简写(--version 简写为 -v)
* type:[ Boolean | String | Number ]
* defaults:默认值
* hide :[ Boolean ] 是否隐藏帮助信息


(四)组合

原文地址:http://yeoman.io/authoring/composability.html

很有趣的是,官网的第一个demo竟然是一个变形金刚组合的gif,可见他们是多么想表达各个小功能组合起来后的yeoman是有多强大。

可以通过以下两种方式开始组合:

  1. 依赖另外一个generator(例如:generator-backbone 使用 generator-mocha)。
  2. 使用者,根据自己的需求在初始化项目的时候选择配置。(例如:sass 或者 less来搭配 webpack 或是 gulp

4.1 generator.composeWith()

composeWith 方法允许你的generator来组合别人的generator,但是一旦组合成功,不要忘记第二章的内容 <(二)Running Context 生命周期>,所有被组合的generator都遵循Yeoman的生命周期规则来顺序执行,不同的generator执行顺序,取决于composeWith调用他们的顺序,看下面的API及执行顺序实例。

4.1.1 API

composeWith接收三个参数:

  1. namespace:声明generator和谁组合。[ String ]
  2. options:调用generator的时候需要接收的参数。[ Object | Array ]
  3. settings:你的generator用这些配置来决定如果运行其他的generators。[ Object ]

    • settings.local:需要在 dependencies中配置,使用dependencies安装的模块相当于本地模块,这里使用 require.resolve来返回一个本地模块的路径,如:node_modules/generator-name [ String ]
    • settings.linkweak or strong [ String ]

      • week link:在初始化的时候不运行,比如后端运行的,frameworks或css的预处理。
      • strong link:一直运行。

当需要用 peerDependencies 来组合generator

this.composeWith('backbone:route', { options: {
  rjs: true
}});

当需要用 dependencies 来组合generator

this.composeWith('backbone:route', {}, {
  local: require.resolve('generator-bootstrap')
});

//注:require.resolve()将返回node.js需要的模块路径。

接下来4.2中会解释 peerDependenciesdependencies的区别。

4.1.2 执行顺序实例
// In my-generator/generators/turbo/index.js
module.exports = require('yeoman-generator').Base.extend({
  'prompting' : function () {
    console.log('prompting - turbo');
  },

  'writing' : function () {
    console.log('writing - turbo');
  }
});

// In my-generator/generators/electric/index.js
module.exports = require('yeoman-generator').Base.extend({
  'prompting' : function () {
    console.log('prompting - zap');
  },

  'writing' : function () {
    console.log('writing - zap');
  }
});

// In my-generator/generators/app/index.js
module.exports = require('yeoman-generator').Base.extend({
  'initializing' : function () {
    this.composeWith('my-generator:turbo');
    this.composeWith('my-generator:electric');
  }
});

来分析下上面这段脚本:

  1. 以上这段脚本在初始化的时候执行了两个 composeWith 方法 turbo 和 electric。
  2. 分别执行了他们目录下的index.js。
  3. 加载顺序判断:turbo 优先于 electric。
  4. 生命周期问题:prompting 优先于 writing。

所以执行后的结果如下:

prompting - turbo
prompting - zap
writing - turbo
writing - zap

4.2 peerDependenciesdependencies的区别

npm允许以下三种dependencies(依赖):

  1. dependencies:使用依赖,自己或是别人使用你的generator所必备的依赖模块。这些模块被generator视为本地模块。
  2. peerDependencies:看下面的 注: npm@3后,peerDependencies不会再被自动安装,需要手动。
  3. devDependencies:开发依赖,作为开发或者是测试需要用的模块,如果别人安装你的generator,这些模块不应该被安装。

当使用 peerDependencies 别的模块也要依赖当前这个模块,小心不要创建版本导致冲突,Yeoman推荐使用(>=) 或 (*) 来安装可用的版本,如:

{
  "peerDependencies": {
    "generator-gruntfile": "*",
    "generator-bootstrap": ">=1.0.0"
  }
}

注:npm@3以后,peerDependencies不会再被自动安装,安装他们必须执行如下:

npm install generator-yourgenerator generator-gruntfile generator-bootstrap@">=1.0.0"

(五)依赖管理

原文地址:http://yeoman.io/authoring/dependencies.html

Yeoman提供以下几种形式来安装依赖。

5.1 npm

使用 generator.npmInstall() 来安装npm包,如果你在多个generators调用了 npm install Yeoman保证只会执行一次。

例如:你需要安装 lodash 这个模块作为发开依赖。

generators.Base.extend({
  installingLodash: function() {
    this.npmInstall(['lodash'], { 'saveDev': true });
  }
});

效果等同于直接在终端输入:

npm install lodash --save-dev

5.2 Bower

使用 generator.bowerInstall() 来安装依赖。实例:同npm。

5.3 Both npm & Bower

使用 generator.installDependencies() 来同时安装npm 和 bower。实例:同npm。

5.4 Using other tools

可以使用 spawnCommand 来安装其他工具。比如:PHP的composer。

(六)文件操作

原文地址:http://yeoman.io/authoring/file-system.html

6.1 根路径

Yeoman会在这个根路径中创建你项目的脚手架。

根路径会以以下两种方式定义:

  1. 当前工作路径
  2. 最近一级中包含 .yo-rc.json 的路径

你可以通过Yeoman提供的 generator.destinationRoot() 方法来获取根路径,这个方法接收一个参数 generator.destinationPath('sub/path') 来获取子目录的路径。

例如:

查看当前路径

$ pwd
~/projects
//跟路径是 ~/projects
generators.Base.extend({
  paths: function () {
    this.destinationRoot();
    // returns '~/projects'

    this.destinationPath('/sub/index.js');
    // returns '~/projects/sub/index.js'
  }
});

6.2 常用工作路径

原文是Template context,其实我感觉直译不太好,换做叫常用工作路径会更好。

这个路径的默认取你当前目录 ./templates/ , 可以手动覆盖这个路径 generator.sourceRoot('new/template/path')

例如:

generators.Base.extend({
  paths: function () {
    this.sourceRoot(); //设置常用工作路径
    // returns './templates'

    this.templatePath('index.js'); //读取常用工作路径
    // returns './templates/index.js'
  }
});

6.3 文件操作

所有文件相关的方法都会通过 this.fs 暴露出来。这里有所有文件操作相关方法,包括下面的copyTpl 方法。

实例: 把一个 常用工作路径 的文件复制到 根路径 下,并传一个参数。

1.常用工作路径下的 ./templates/index.html 内容是:
<html>
  <head>
    <title><%= title %></title>
  </head>
</html>
2.我们用 copyTpl 方法把来复制文件,该方法使用ejs模板语法
generators.Base.extend({
  writing: function () {
    this.fs.copyTpl(
      this.templatePath('index.html'),//第一个参数:from
      this.destinationPath('public/index.html'),//第二个参数:to
      { title: 'Templating with Yeoman' }//第三个参数:options
    );
  }
});
3.来看复制后的 public/index.html
<html>
  <head>
    <title>Templating with Yeoman</title>
  </head>
</html>

6.4 通过「流」来改变文件

Yeoman提供 registerTransformStream() 方法,使用gulp的来操作文件。

例如:

var beautify = require('gulp-beautify');
this.registerTransformStream(beautify({indentSize: 2 }));

6.5 修改已经存在文件的内容

Yeoman介绍了几个比较流行的解析器:

1.Cheerio for parsing HTML,基本实现流程如下
var cheerio = require('cheerio'),
    $ = cheerio.load('<h2 class="title">Hello world</h2>');

$('h2.title').text('Hello there!');
$('h2').addClass('welcome');

$.html();
//=> <h2 class="title welcome">Hello there!</h2>
2. Esprima for parsing JavaScript
3. For JSON files 可以使用JSON原生的方法
本次只翻译了前六章,后续会翻译后六章、自己如果写一个generator以及遇到的坑和问题。都会更新在我的github的Yeoman-article中。

github:https://github.com/tonyljl526/yeoman


阿驴
328 声望19 粉丝