Yeoman
起步
yeoman
的工作流程很简单,使用yeoman
运行一个类似插件的generator
,我们称之为生成器(发电机),就可以很快构搭建起一个特定的工程。
首先是全局安装yeoman
模块:
npm install -g yo
然后找到一个符合你工程需求的generator
,本质上也是一个npm
包,因此同样安装在全局目录:
npm install -g generator-webapp
然后执行yoman
的脚手架启动指令:
yo webapp
执行过程中会有命令行输入信息,以供你选择符合工程设计的文件选项,最终你就得到了一个完整的脚手架工程,达到开箱即用的效果。
generator
生成器是 Yeoman 生态系统的基石。它们是
yo
为最终用户生成文件所运行的插件。
可以发现,yeoman
是执行脚手架的平台,真正生成脚手架的逻辑是generator
所控制的,这种插件流的方式正是其通用的基础。
官方提供了一些实用的genarator
:official_genarators,我们可以从中找到适合预期工程的生成器,如果不符合我们还可以自定义generator
,具体细节可参考:yeoman.io/authoring/。
自定义generator
生成器的核心是一个 Node.js 模块。
其实官方文档已经写得十分详尽了,下面作为实例进行演示。
generator基本的组织结构
由于generator的核心是一个node.js模块,因此我们先创建一个目录,并进行npm
初始化,但需要注意的是:
- 包名即文件夹名称必须为
generator-name
的格式,其中name
为生成器的名称。 npm init
生成的package.json
文件中,keywords
属性必须包含"yeoman-generator"
并且 repo 必须有一个描述,以便生成器页面索引。- 确保将最新版本设置
yeoman-generator
为依赖项。 files
属性必须是生成器使用的文件和目录数组。
{
"name": "generator-name",
"version": "0.1.0",
"description": "",
"files": [
"generators"
],
"keywords": ["yeoman-generator"],
"dependencies": {
"yeoman-generator": "^1.0.0"
}
}
generator文件树
除了上述模块信息的要求,Yeoman 的功能取决于如何构建目录树。每个子生成器都包含在其自己的文件夹中。
调用时使用的默认生成器yo name
是app
生成器。这必须包含在app/
目录中。调用时使用的子生成器yo name:subcommand
存储在与 sub 命令完全相同的文件夹中。
目录树可能是这样的:
├───package.json
└───generators/
├───app/
│ └───index.js
└───router/
└───index.js
Yeoman 允许两种不同的目录结构。它将查找./
并generators/
注册可用的生成器。所以目录树还可能是这样的:
├───package.json
├───app/
│ └───index.js
└───router/
└───index.js
但这时需要确保package.json
中的files
字段包含目录信息:
{
"files": [
"app",
"router"
]
}
基本的模块结构
生成器的入口文件:generators/app/index.js
,模块需要导出一个继承Generator
对象的子类对象:
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.log('App init');
}
method() {
this.log('method is run');
}
method2() {
return new Promise((res, rej) => {
this.log('method2 is run');
setTimeout(() => {
this.appname = 'new appname';
res();
}, 100);
});
}
method3() {
this.log('appname=>', this.appname);
}
};
值得注意的是,generator的函数是从Object.getPrototypeOf(Generator)
自动调用的,对于异步函数,返回一个Promise
对象即可,如果想要避免函数被调用,使用_
前缀作为函数命名即可。
用户交互
通过询问用户来获取用户的项目偏好以及更新一些用户设置。yoman
中使用prompt
函数来完成该任务,该工具函数是Inquirer.js
的封装工具函数。
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.log('App init');
}
prompting() {
return this.prompt([{
type: "input",
name: "title",
message: "Your project name?",
default: this.appname
},
{
type: "confirm",
name: "cool",
message: "Would you like to enable the Cool feature?"
}]).then(answer => {
this.answer = answer;
});
}
};
文件交互
生成器的最终目的就是要创建文件,yoman
封装了fs
模块并提供fs
工具函数便于文件相关的操作。
const Generator = require('yeoman-generator')
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.log('App init');
}
writing() {
const temp = this.templatePath('index.html'); // 模块路径(默认路径为生成器项目下的genrators/templates目录)
const dest = this.destinationPath('pages/index.html'); // 目标路径(默认路径为当前执行环境)
this.fs.copyTpl(temp, dest, this.answer);
}
};
copyTpl
函数采用ejs
模板引擎的方式对文件进行模板渲染。
发布generator模块
用户交互和文件交互逻辑完成后,生成器的逻辑基本就已经完成了,为了方便使用,你可以将该生成器发布到npm
官网,这和发布普通模块没什么区别。
Polp
Plop 是一个小工具,可以节省您的时间并帮助您的团队以一致的方式构建新文件。
Plop
比yoman
更小的小而美的脚手架工具,甚至你都可以不认为它是一个脚手架工具,而只是在项目中用于快速复用文件的小工具。
Plop
的使用逻辑同yoman
基本一致:询问用户,然后根据用户输入信息写入文件。
简单使用
首先在项目中安装Plop
模块:
npm install --save-dev plop
在项目根目录创建一个plopfile.js
的入口文件:
module.exports = function (plop) {
// controller generator
plop.setGenerator('controller', {
description: 'application controller logic',
// 用户交互
prompts: [{
type: 'input',
name: 'name',
message: 'controller name please'
}],
// 文件交互
actions: [{
type: 'add',
path: 'src/{{name}}.js',
templateFile: 'plop-templates/controller.hbs'
}]
});
};
然后在package.json
中添加脚本plop
即可运行。
脚手架的工作原理
可以发现,脚手架的本质就是一个cli
应用,通过与用户交互,根据用户偏好来设置预设好的工程脚手架文件,自动帮用户创建好这些文件。
现在我们就简单模拟一下这个过程。
创建一个node模块:
mkdir scaffold-application
cd scaffold-application
npm init -y
编写package.json
的bin
字段指定cli
的入口文件:
{
bin: "cli.js"
}
编写cli.js
cli
应用和node.js
没什么区别,主要是多了一个固定文件头\#!/usr/bin/env node
作为标识。
#!/usr/bin/env node
// 使用inquirer.js进行用户交互
const inquirer = require('inquirer');
const path = require('path');
const ejs = require('ejs');
const fs = require('fs');
inquirer.prompt({
type: 'input',
name: 'name',
message: 'App name',
}).then(anwser => {
// 通过ejs进行模板渲染,然后使用fs模块写入文件
const tempDir = path.resolve(__dirname, 'templates');
const destDir = process.cwd();
fs.readdir(tempDir, (err, files) => {
if (err) throw err
files.forEach(file => {
ejs.renderFile(path.join(tempDir, file), anwser, (err, res) => {
if (err) throw err
fs.writeFileSync(path.join(destDir, file), res);
});
});
});
})
然后通过npm link
link到全局,然后就可以通过scaffold-application
命令使用了。