跳到主要内容

2 篇博文 含有标签「脚手架」

查看所有标签

· 15 分钟阅读

Yeoman 是一个开源的客户端脚手架生成工具,用于web应用开发。它帮助开发者快速搭建新的项目,提供了一套标准化的工具和流程来加速开发。Yeoman 本身是基于Node.js的,它通过脚手架(scaffolding)、任务运行器(如Grunt和Gulp)以及包管理(如Bower和npm)等工具,帮助开发者自动化常见的开发任务,比如创建项目结构、添加库依赖、运行测试等。Yeoman 主要用于以下几种场景:

  • 快速启动新项目:Yeoman 可以快速生成项目的基础结构,包括目录、文件以及代码模板,这对于想要迅速开始编码的开发者来说非常有用。

  • 保持项目结构一致性:在团队协作中,Yeoman 可以确保每个新项目或组件都遵循相同的目录结构和编码规范,有助于维护代码的一致性和可读性。

  • 自动化工作流:Yeoman 可以集成任务运行器(如Grunt或Gulp)和包管理器(如npm或Bower),自动化重复性的任务,如压缩图片、编译CSS和JavaScript、运行测试等。

  • 快速集成前端库和框架:Yeoman 脚手架通常会包含流行的前端库和框架,如React、Angular、Vue等,这样开发者就不需要手动下载和配置这些依赖。

  • 创建自定义生成器:如果现有的脚手架不能满足特定需求,开发者可以创建自定义的Yeoman生成器,以适应特定的项目结构或编码规范。

总的来说,Yeoman 是一个适用于多种开发场景的工具,特别是在需要快速搭建和维护前端项目结构时。

创建generator

目录结构

创建一个文件夹,该文件夹必须命名为generator-name(其中name是generator的名称)。Yeoman 依赖文件系统来查找可用的generator。在该文件夹下创建package.json,并执行npm install --save yeoman-generator

package.json
{
"name": "generator-demo",
"version": "0.1.0",
"description": "",
"files": [
"generators"
],
"keywords": ["yeoman-generator"],
"dependencies": {
"yeoman-generator": "^1.0.0"
}
}

当您调用 yo name 时使用的默认generator是app generator。它必须包含在 app 目录中。当您调用 yo name:subcommand 时使用的sub generator包含在subcommand目录中。假如generator-demo目录结构如下(package.jsonfiles属性的值为["generators"]),则执行npm install -g yo generator-demo安装yogenerator-demo后,可以执行yo demoyo demo:router

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

目录结构还可以如下,此时package.jsonfiles属性的值应为["app","router"]

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

扩展yeoman-generator

Yeoman 提供了一个基础生成器,您可以扩展它来实现您自己的行为。这个基本生成器将添加您期望减轻任务的大部分功能。

generators/app/index.js
const Generator = require('yeoman-generator');

module.exports = class extends Generator {};

某些生成器方法只能在构造函数内部调用。这些特殊方法可能会执行诸如设置重要状态控制之类的操作,并且可能无法在构造函数之外运行。要重写生成器构造函数,请添加一个构造函数方法,如下所示:

generators/app/index.js
const Generator = require('yeoman-generator');

module.exports = class extends Generator {
constructor(args, opts) {
// Calling the super constructor is important so our generator is correctly set up
super(args, opts);

this.option('babel'); // This method adds support for a `--babel` flag
}

method1() {
this.log('method 1 just ran');
}

method2() {
this.log('method 2 just ran');
}
};

运行generator

每次运行generator时,实际上都在使用yeoman-environmentnpm install --save yeoman-environment

const yeoman = require('yeoman-environment');
const env = yeoman.createEnv();

注册generator-demo,有两种方式:

const yeoman = require('yeoman-environment');
const env = yeoman.createEnv();

// 1. 基于路径注册generator,namespace('npm:app')是可选的
env.register(require.resolve('generator-npm'), 'npm:app');

// 2. 提供 generator constructor,这种方式需要提供namespace
const GeneratorNPM = generators.Base.extend(/* put your methods in here */);
env.registerStub(GeneratorNPM, 'npm:app');

可以根据需要注册任意数量的generator。如果namespace发生冲突,本地generator将覆盖全局generator。

运行generator,您只需将如下代码放入 package.json bin 指定的文件中,即可运行 Yeoman generator,而无需使用 yo。本地开发使用npm link调试。

const yeoman = require('yeoman-environment');
const env = yeoman.createEnv();

env.register(require.resolve('generator-npm'), 'npm:app');

const done = function() {
console.log('done')
}
env.run('npm:app', done); // 可以设置options: env.run('npm:app some-name', { 'skip-install': true }, done);
tip

在使用脚手架工具(如Yeoman)创建项目时,你可能不希望立即安装所有依赖项,而是首先调整生成的项目结构或配置。在这种情况下,你可以使用'skip-install': true的配置或命令行选项来跳过自动安装依赖的步骤。

使用lookup获取npm安装的每个Yeoman generator的访问权限,每个查找到的generator会被注册

const yeoman = require('yeoman-environment');
const env = yeoman.createEnv();

env.lookup(function () {
env.run('angular');
});

方法执行顺序

直接附加到 Generator 原型的每个方法都被视为一个任务。每个任务都由 Yeoman environment 按顺序循环运行。

可用的优先级是(按运行顺序):

  1. initializing,初始化方法(检查当前项目状态、获取配置等)
  2. prompting,提示用户选项的地方(调用 this.prompt() 的地方)
  3. configuring,保存配置并配置项目(创建 .editorconfig 文件和其他元数据文件)
  4. default,如果方法名称与优先级不匹配,它将被推送到该组。
  5. writing,编写生成器特定文件(路线、控制器等)的地方
  6. conflicts,处理冲突的地方(内部使用)
  7. install,运行安装的地方(npm、bower)
  8. end,最后调用,用于清理等

如何定义不会自动调用的辅助方法或私有方法?有三种不同的方法可以实现这一目标:

  • 用下划线作为方法名称前缀

    class extends Generator {
    method1() {
    console.log('hey 1');
    }

    _private_method() {
    console.log('private hey');
    }
    }
  • 使用实例方法

    class extends Generator {
    constructor(args, opts) {
    // Calling the super constructor is important so our generator is correctly set up
    super(args, opts)

    this.helperMethod = function () {
    console.log('won\'t be called automatically');
    };
    }
    }
  • 继承父生成器

    class MyBase extends Generator {
    helper() {
    console.log('methods on the parent generator won\'t be called automatically');
    }
    }

    module.exports = class extends MyBase {
    exec() {
    this.helper();
    }
    };

用户交互

Yeoman 使用适配器作为抽象层,以允许 IDE、代码编辑器等轻松提供运行生成器所需的用户界面。

适配器是负责处理与用户的所有交互的对象。如果您想提供与经典命令行不同的交互模型,您必须编写自己的适配器。与用户交互的每种方法都通过此适配器传递(主要是:提示、日志记录和比较)。

要安装适配器,请使用 yeoman.createEnv(args, opts, adapter) 的第三个参数。适配器应至少提供三种方法:

  • prompt(),它提供问答功能(例如,当您开始时,会向用户提示一组可能的操作)。它的签名和行为遵循 Inquirer.js 的签名和行为。当生成器调用 this.prompt 时,该调用最终由适配器处理。该方法是异步的,返回一个Promise
  • diff()
  • log(),输出信息 Yeoman 默认提供了终端适配器(Terminal Adapter),可以使用this.prompt this.log this.diff
module.exports = class extends Generator {
async prompting() {
this.answers = await this.prompt([
{
type: "input",
name: "name",
message: "Your project name",
default: this.appname // Default to current folder name
},
{
type: "confirm",
name: "cool",
message: "Would you like to enable the Cool feature?"
}
]);

this.log("app name", answers.name);
this.log("cool feature", answers.cool);
}

writing() {
this.log("cool feature", this.answers.cool); // user answer `cool` used
}
};

记住用户偏好

用户每次运行生成器时可能会对某些问题给出相同的输入。对于这些问题,您可能想要记住用户之前回答的内容,并将该答案用作新的默认答案。

Yeoman 通过向问题对象添加 store 属性来扩展 Inquirer.js API。此属性允许您指定用户提供的答案应用作将来的默认答案。这可以按如下方式完成:

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

参数

参数直接从命令行传递,比如:yo webapp my-project

module.exports = class extends Generator {
// note: arguments and options should be defined in the constructor.
constructor(args, opts) {
super(args, opts);

// This makes `appname` a required argument.
this.argument("appname", { type: String, required: true });

// And you can then access it later; e.g.
this.log(this.options.appname);
}
};

选项

使用--,比如:yo webapp --coffee

module.exports = class extends Generator {
// note: arguments and options should be defined in the constructor.
constructor(args, opts) {
super(args, opts);

// This method adds support for a `--coffee` flag
this.option("coffee");

// And you can then access it later; e.g.
this.scriptSuffix = this.options.coffee ? ".coffee" : ".js";
}
};

位置上下文

.yo-rc.json

.yo-rc.json 文件是 Yeoman 生成器使用的配置文件。Yeoman 是一个通用的前端项目脚手架工具,用于自动化项目的搭建过程,比如创建新的项目、添加新的模块等。.yo-rc.json 文件存储了关于项目的配置信息,使得项目的结构和依赖能够被重现,这对于团队合作和项目的一致性非常重要。

这个文件通常位于项目的根目录,并且包含了用于生成项目的 Yeoman 生成器的配置选项。这些配置选项可以包括项目名称、版本、使用的技术栈、编码风格偏好等。

例如,如果你使用了一个名为 generator-webapp 的 Yeoman 生成器来创建一个 web 应用,.yo-rc.json 文件可能看起来像这样:

{
"generator-webapp": {
"appName": "My Awesome App",
"ui": {
"key": "value"
},
"wiredep": {
"directory": "bower_components"
}
}
}

这个文件的具体内容将取决于你使用的具体生成器以及在项目初始化过程中所做的选择。

当你或你的团队成员在项目中运行 Yeoman 生成器时,Yeoman 会查找 .yo-rc.json 文件,并使用其中的配置来确保生成的代码和结构与项目的其他部分保持一致。这样做有助于维护项目的一致性和可维护性。

根目录

可以使用 this.destinationRoot() 获取项目根目录 或 使用 this.destinationPath('sub/path') 获取根目录下的文件

// Given destination root is ~/projects
class extends Generator {
paths() {
this.destinationRoot();
// returns '~/projects'

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

模板目录

模板上下文是存储模板文件的文件夹。模板上下文默认定义为 ./templates/。您可以使用 this.sourceRoot('new/template/path') 覆盖此默认值。

您可以使用 this.sourceRoot() 或使用 this.templatePath('app/index.js') 加入路径来获取路径值。

class extends Generator {
paths() {
this.sourceRoot();
// returns './templates'

this.templatePath('index.js');
// returns './templates/index.js'
}
};

this.fs

例如,使用 this.fs.copyTpl 方法复制文件,同时将内容作为模板进行处理。 copyTpl 使用 ejs 模板语法。

generators/app/templates/index.html
<html>
<head>
<title><%= title %></title>
</head>
</html>
class extends Generator {
writing() {
this.fs.copyTpl(
this.templatePath('index.html'),
this.destinationPath('public/index.html'),
{ title: 'Templating with Yeoman' }
);
}
}
public/index.html
<html>
<head>
<title>Templating with Yeoman</title>
</head>
</html>

一个非常常见的场景是在提示阶段存储用户答案并将其用于模板:

class extends Generator {
async prompting() {
this.answers = await this.prompt([{
type : 'input',
name : 'title',
message : 'Your project title',
}]);
}

writing() {
this.fs.copyTpl(
this.templatePath('index.html'),
this.destinationPath('public/index.html'),
{ title: this.answers.title } // user answer `title` used
);
}
}
tip

更新预先存在的文件并不总是一项简单的任务。最可靠的方法是解析文件 AST(抽象语法树)并对其进行编辑。此解决方案的主要问题是编辑 AST 可能很冗长并且有点难以掌握。

一些流行的 AST 解析器是:

  • Cheerio 用于解析 HTML。

  • 用于解析 JavaScript 的 Esprima - 您可能对 AST-Query 感兴趣,它提供了较低级别的 API 来编辑 Esprima 语法树。

对于 JSON 文件,您可以使用本机 JSON 对象方法

Gruntfile Editor 用于动态修改 Gruntfile。

使用 RegEx 解析代码文件是一条危险的道路,在此之前,您应该阅读此 CS 人类学答案并掌握 RegEx 解析的缺陷。如果您选择使用 RegEx 而不是 AST 树编辑现有文件,请小心并提供完整的单元测试。 - 请不要破坏您的用户的代码。

· 9 分钟阅读

commander chalk ora

commander, chalk, 和 ora 是三个常用的 Node.js 库,它们分别用于命令行界面的命令解析、终端字符串样式和命令行加载动画。以下是每个库的介绍及其基本用法。

Commander

commander 是一个用于 Node.js 命令行界面的命令解析库。它可以帮助你轻松地定义和解析命令行参数和选项。

安装

npm install commander

基本用法

const { Command } = require('commander');
const program = new Command();

program
.version('1.0.0')
.description('An example CLI for demonstration purposes')
.option('-n, --name <type>', 'specify your name')
.option('-a, --age <number>', 'specify your age', parseInt);

program.parse(process.argv);

const options = program.opts();
if (options.name) console.log(`Hello, ${options.name}!`);
if (options.age) console.log(`You are ${options.age} years old.`);

Chalk

chalk 是一个用于终端字符串样式的库。它可以帮助你为终端输出添加颜色和样式。

安装

npm install chalk

基本用法

const chalk = require('chalk');

console.log(chalk.blue('Hello world!'));
console.log(chalk.red.bold('Error!'));
console.log(chalk.green.underline('Success!'));
console.log(chalk.yellow.bgRed.bold('Warning!'));

Ora

ora 是一个用于在命令行界面中显示加载动画的库。它可以帮助你在执行异步操作时显示一个旋转的加载指示器。

安装

npm install ora

基本用法

const ora = require('ora');

const spinner = ora('Loading...').start();

setTimeout(() => {
spinner.color = 'yellow';
spinner.text = 'Loading something else...';
}, 1000);

setTimeout(() => {
spinner.succeed('Loading complete');
}, 3000);

综合示例

下面是一个综合示例,展示如何将 commander, chalk, 和 ora 结合使用,创建一个简单的命令行工具。

安装依赖

npm install commander chalk ora

代码示例

const { Command } = require('commander');
const chalk = require('chalk');
const ora = require('ora');

const program = new Command();

program
.version('1.0.0')
.description('An example CLI for demonstration purposes')
.option('-n, --name <type>', 'specify your name')
.option('-a, --age <number>', 'specify your age', parseInt);

program.parse(process.argv);

const options = program.opts();
const spinner = ora('Processing...').start();

setTimeout(() => {
spinner.succeed('Processing complete');

if (options.name) {
console.log(chalk.blue(`Hello, ${options.name}!`));
} else {
console.log(chalk.red('Name not specified.'));
}

if (options.age) {
console.log(chalk.green(`You are ${options.age} years old.`));
} else {
console.log(chalk.red('Age not specified.'));
}
}, 2000);

在这个示例中,我们使用 commander 解析命令行参数,使用 ora 显示加载动画,并使用 chalk 为终端输出添加颜色和样式。运行这个脚本时,可以使用以下命令:

node script.js --name John --age 30

这将输出带有颜色和样式的文本,并在处理过程中显示加载动画。

inquirer(推荐使用@inquirer/prompts)

inquirer 是一个用于创建交互式命令行界面(CLI)的 Node.js 库。它提供了一种简单的方式来与用户进行交互,通过命令行提示用户输入信息、选择选项等。

安装

首先,你需要安装 inquirer

npm install inquirer

基本用法

以下是一个基本示例,展示如何使用 inquirer 提示用户输入信息并处理用户的输入:

const inquirer = require('inquirer');

const questions = [
{
type: 'input',
name: 'name',
message: 'What is your name?',
},
{
type: 'number',
name: 'age',
message: 'How old are you?',
},
{
type: 'list',
name: 'favoriteColor',
message: 'What is your favorite color?',
choices: ['Red', 'Green', 'Blue', 'Yellow'],
},
];

inquirer.prompt(questions).then((answers) => {
console.log(`Hello, ${answers.name}!`);
console.log(`You are ${answers.age} years old.`);
console.log(`Your favorite color is ${answers.favoriteColor}.`);
});

常见的提示类型

inquirer 支持多种提示类型,包括:

  • input: 用于接收用户的输入。
  • number: 用于接收数字类型的输入。
  • confirm: 用于接收布尔值(是/否)。
  • list: 用于让用户从多个选项中选择一个。
  • rawlist: 类似于 list,但选项使用数字索引。
  • expand: 类似于 list,但使用单个字符作为快捷键。
  • checkbox: 用于让用户从多个选项中选择多个。
  • password: 用于接收密码类型的输入,输入内容会被隐藏。
  • editor: 用于打开默认文本编辑器让用户输入长文本。

提示类型示例

以下是一些常见提示类型的示例:

const inquirer = require('inquirer');

const questions = [
{
type: 'input',
name: 'username',
message: 'What is your username?',
},
{
type: 'password',
name: 'password',
message: 'Enter your password',
},
{
type: 'confirm',
name: 'confirm',
message: 'Are you sure?',
},
{
type: 'list',
name: 'theme',
message: 'What is your preferred theme?',
choices: ['Light', 'Dark', 'Solarized'],
},
{
type: 'checkbox',
name: 'features',
message: 'Select the features you want:',
choices: [
'Feature A',
'Feature B',
'Feature C',
],
},
];

inquirer.prompt(questions).then((answers) => {
console.log('Answers:', answers);
});

报错

inquirer.prompt(...).then is not a function 解决:使用v8版本(v9开始不再兼容CJS) 或者使用@inquirer/prompts(兼容CJS)

综合示例

下面是一个综合示例,展示如何将 commander, chalk, ora, 和 inquirer 结合使用,创建一个更复杂的命令行工具。

安装依赖

npm install commander chalk ora inquirer

代码示例

const { Command } = require('commander');
const chalk = require('chalk');
const ora = require('ora');
const inquirer = require('inquirer');

const program = new Command();

program
.version('1.0.0')
.description('An example CLI for demonstration purposes')
.option('-i, --interactive', 'run in interactive mode');

program.parse(process.argv);

const options = program.opts();

if (options.interactive) {
const questions = [
{
type: 'input',
name: 'name',
message: 'What is your name?',
},
{
type: 'number',
name: 'age',
message: 'How old are you?',
},
{
type: 'list',
name: 'favoriteColor',
message: 'What is your favorite color?',
choices: ['Red', 'Green', 'Blue', 'Yellow'],
},
];

inquirer.prompt(questions).then((answers) => {
console.log(chalk.blue(`Hello, ${answers.name}!`));
console.log(chalk.green(`You are ${answers.age} years old.`));
console.log(chalk.yellow(`Your favorite color is ${answers.favoriteColor}.`));
});
} else {
const spinner = ora('Processing...').start();

setTimeout(() => {
spinner.succeed('Processing complete');
console.log(chalk.red('Run the script with --interactive to provide inputs.'));
}, 2000);
}

在这个综合示例中,我们使用 commander 解析命令行参数,使用 ora 显示加载动画,使用 inquirer 提示用户输入信息,并使用 chalk 为终端输出添加颜色和样式。运行这个脚本时,可以使用以下命令:

node script.js --interactive

这将启动交互模式,提示用户输入信息。

Ctrl+C 中断不报错退出

为了优雅地处理 Ctrl+C 退出并避免报错,我们需要捕获 inquirer 库抛出的 ExitPromptError 异常。我们可以在捕获异常时进行特定处理,从而避免程序报错。

以下是一个示例,展示如何捕获并处理 ExitPromptError 异常:

const inquirer = require('@inquirer/prompts');

// 捕获 SIGINT 信号 (Ctrl+C)
process.on('SIGINT', () => {
console.log('\n操作已取消');
process.exit(0); // 正常退出
});

async function promptUser() {
try {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: '请输入你的用户名:',
},
{
type: 'password',
name: 'password',
message: '请输入你的密码:',
},
]);
console.log('你的输入:', answers);
} catch (error) {
if (error.isTtyError) {
console.error('Prompt couldn't be rendered in the current environment');
} else if (error.message === 'User force closed the prompt with 0 null') {
console.log('用户中断了输入');
} else {
console.error('发生错误:', error);
}
}
}

promptUser();

解释:

  • 捕获 SIGINT 信号

    • 使用 process.on('SIGINT', ...) 监听 SIGINT 信号,这个信号在用户按下 Ctrl+C 时触发。
    • 在信号处理函数中,输出一条消息(例如 操作已取消),然后调用 process.exit(0) 正常退出程序。process.exit(0) 表示正常退出,而非错误退出。
  • 处理用户输入

    • 使用 inquirer.prompt 提示用户输入。
    • 如果用户按下 Ctrl+C,程序会抛出一个 ExitPromptError 异常。
    • catch 块中捕获这个异常,并根据异常信息进行特定处理:
      • 如果异常信息为 User force closed the prompt with 0 null,则表示用户中断了输入,此时可以输出一条友好的消息(例如 用户中断了输入),而不是抛出错误。
      • 其他异常则按常规处理。

通过这种方式,你可以确保在用户按下 Ctrl+C 时,程序能够优雅地退出,并且不会抛出未处理的错误。