篇六:前端工程化之脚手架笔记

1,231 阅读19分钟

文章输出主要来源:拉勾大前端高新训练营(链接) 与 各技术官网。小哥哥小姐姐请不要嫌弃啰嗦,下面肯定都是干货。

1. 脚手架工具

脚手架工具可以方便地帮助我们进行项目基本结构的生成。

它是对具有相同的代码组织结构、相同开发范式、相同模块依赖、相同工具配置、相同基础代码的一类项目的快速生成工具。

常用脚手架工具:

专用型项目脚手架工具

  • Vue: vue-cli
  • React: create-react-app
  • Angular: angular-cli

通用性脚手架工具

  • Yeoman:可以通过yeoman自定义自己需要的脚手架工具

创建局部文件类型的工具

  • plop: 根据具体的模板生成相应的代码文件,使用场景为快速生成新组件或模块等场景。

2. Yeoman

Yeoman官方文档

Yeoman是一款通用的脚手架系统,允许创建任何类型的应用程序。它是与语言无关的脚手架工具,Web、Java、Python、C#等类型的项目都可以使用Yeoman作为脚手架。

我们可以使用其他人制作的Yeoman脚手架工具,也可以定制自己的脚手架工具。

2.1 Yeoman 脚手架的使用

通过Yeoman创建的脚手架使用需要先安Yeoman,然后再安装具体的脚手架工具。最终按照具体的脚手架文档进行使用。

  • 安装Yeoman: npm install -g yo
  • 安装使用Yeoman编写的脚手架工具: npm install -g generator-name ,例如npm install -g generator-webapp
  • 使用脚手架工具创建具体的项目: yo name 例如: yo webapp

sub-generator: 除了创建工程的脚手架,Yeoman还提供了sub-generator用于创建模块或组件,例如yo angular:controller MyNewController可以为项目添加新的controller

2.2 自定义Generator

官网说,一个generator的核心就是一个node.js模块。

它一般以一个Npm包的形式出现,在创建一个generator的时候,项目名称必须以generator-name的格式进命名,generator-前缀必须有,后面name才是生成器的名称。

generator的基本项目结构为:

├───package.json
└───generators/  -------------生成器目录
    ├───app/ -----------------默认生成器目录
    │   └───index.js ---------默认生成器实现
    └───router/ --------------其他(sub-generator)生成器目录
        └───index.js ---------生成器实现

package.json中,除了项目名称必须为gererator-name的形式,keywords字段里面也必须包含yeoman-generator关键词:

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

创建基础generator流程与示例:

  1. 创建项目目录mkdir generator-sample & cd generator-sample

  2. 初始化项目yarn init or npm init

  3. 安装yeoman-generator yarn add yeoman-generator or npm install yeoman-generator

  4. 创建默认生成器目录mkdir -p generators/app & touch generators/app/index.js,该文件将作为默认生成器的入口文件

  5. Yeoman提供了一个基础的Generator类,我们可以继承它实现自己的一些行为,每次调用生成器,被添加到类prototype中的方法也都会被当做任务,执行一次,通常是按顺序执行,某些特殊的方法可能会触发特定的顺序

    //generators/app/index.js
    const Generator = require('yeoman-generator');
    
    module.exports = class extends Generator {
      // 重写构造函数,构造函数中可能会运行一些特殊的方法,例如设置重要的状态控件,而它们无法再构造函数外部执行
      constructor(args, opts) {
        super(args, opts);
      }
      sayHello() { // 在使用脚手架生成项目时会先打印hello world,prototype中的普通方法会依次按顺序执行
        console.log('hello world')
      }
      // 简单的写入文件操作
      writing() {
        this.fs.write(
          this.destinationPath('temp.txt'),
          Math.random().toString()
        )
      }
    }
    
  6. 创建Npm包链接yarn link or npm link

  7. 使用yo sample即可创建项目,生成temp.txt文件。

2.3 Yeoman中与文件系统交互

Yeoman中文件操作方法基于磁盘上的两个位置上下文location contexts,它们是generator用于读取和写入操作的两个文件夹位置。分别为Destination context 目标上下文Template context 模板上下文

2.3.1 Destination context 目标位置上下文

  • 目标位置上下文中的目标是 Yeoman 脚手架生成新应用的位置。

  • 目标上下文被定义在当前工作目录中的.yo-rc.json或最近的父文件夹中的.yo-rc.json中。

  • .yo-rc.json中定义了Yeoman工程的根目录.(其中还可以配置其他选项)

通过this.destinationRoot()可以获取目标位置,通过this.destinationPath('index.js')可以自定义目标位置,但官方表明为了确保一致性,不推荐修改目标位置。

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

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

2.3.2 Template context 模板位置上下文

  • 模板位置上下文中的模板位置默认为generator同级目录下的./templates/,例如your-project/generators/app/templates
  • 通过this.sourceRoot()可以获取到模板位置的绝对路径
  • 通过this.templatePath('app/index.js')将子目录与模板目录拼接,获取到模板目录/子目录
// yeoman官方例子
class extends Generator {
  paths() {
    this.sourceRoot();
    // returns './templates'

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

2.3.3 文件的读写

  • Yeoman在创建项目时,对预先已经存在的文件处理会比较小心,默认不会进行覆盖,而是通过命令行报出冲突并询问是否进行覆盖。

  • 由于涉及到用户的交互,可能需要等待用户的输入,这就意味着对文件的写入操作需要设计为异步模式。

  • 由于异步API难以使用,Yeoman提供了一个同步文件系统API,其中每个文件都被写到内存中的文件系统in-memory file system中,并且在Yeoman运行完后只写到磁盘一次

2.3.4 文件操作方法

generator中操作文件的方法都通过this.fs进行暴露,this.fsmem-fs editor的一个对象实例,点击可查看其所有方法说明。

其中复制模板apithis.fs.copyTpl可以处理模板文件,其使用了ejs模板语法,因此在定制模板时可以采用 ejs template syntax方案

2.3.5 通过模板复制文件案例

  1. 定义脚手架模板
<!--generators/app/templates/index.html-->
<html>
  <head>
    <title><%= title %></title>
  </head>
  <body>
    hello <%= name %>
  </body>
</html>
  1. 使用copyTpl复制模板并传入变量,使用方式:copyTpl(from, to, context[, templateOptions [, copyOptions]]),详情见mem-fs editor

    // generators/app/index.js
    const Generator = require('yeoman-generator');
    
    module.exports = class extends Generator {
      writing() {
        this.fs.copyTpl(
          this.templatePath('index.html'),
          this.destinationPath('public/index.html'), // 设置目标上下文位置
          { title: 'Say Hello', name: 'Tom' }
        );
      }
    }
    
  2. npm link or yarn link生成全局软链接

  3. 在自己的项目中使用脚手架yo sample , 输出create public/index.html,成功创建public/index.html,内容为:

    <html>
      <head>
        <title>Say Hello</title>
      </head>
      <body>
        hello Tom
      </body>
    </html>
    

通过以上案例,我们已经明确如何使用模板进行创建文件,这在普通的脚手架工程里面也是非常常用的创建文件方式。

2.4 Generator运行时上下文

当前小结为在写2.5时候进行插入的,在了解命令行中与用户交互之前,我们还需要了解一下generator中的一些特定的方法。

在之前说到过添加到Generator的prototype中的方法都会作为任务,也就是说只要能通过Object.getPrototypeOf(Generator)获取到的方法都是generator的任务,在使用generator时候自动执行。其中普通的方法会按顺序依次执行,但有些特殊的方法会有特定的执行时机。

2.4.1 不会被当做任务的方法

  • 私有方法不会被当做任务: 上面提到能被Object.getPrototypeOf(Generator)获取到的方法都是generator的任务,其中有个特例为私有方法以_开头的方法不会被当做任务

    // yeoman官方给出的示例代码
    class extends Generator {
       // 会被当做任务自动执行
       method1() {
         console.log('hey 1');
       }
    
       // 不会被当做任务自动执行
       _private_method() {
         console.log('private hey');
       }
     }
    
  • 实例方法不会被当做任务执行:

    // yeoman官方给出的示例代码
    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');
         };
       }
     }
    
  • 继承自父Generator的方法不会被当做任务:

    // yeoman官方给出的示例代码  
    	class MyBase extends Generator {
         helper() {
           console.log('methods on the parent generator won\'t be called automatically');
         }
       }
    
       module.exports = class extends MyBase {
         // exec不会被当做任务自动执行
         exec() {
           this.helper();
         }
       };
    

2.4.2 run loop队列系统

由于Yeoman脚手架项目中可以存在多个generator,并可以进行组合compose,详情可查阅文档。在单个generator的情况下顺序执行任务是可以的,但使用组合生成器就不行了,因此使用了run loop方式。

run loop是一个具有优先级支持的队列系统,Yeoman采用了 Grouped-queue 模块进行run loop的实现。

优先级可以通过我们自行指定,如果prototype中的方法命中优先级的方法名,则会被放入到特殊的队列special queue.如果没有命中,则会被push进默认组default group

这也解释了之前我们说的普通的任务会按顺序依次执行,特殊的任务会打破这个规则。

例:

// Yeoman官方给出的示例代码
// 定义单个单优先级任务
class extends Generator {
  priorityName() {}
}

// 成组定义多个优先级任务,注意它不能使用js的class语法定义gererator
Generator.extend({
  priorityName: {
    method() {},
    method2() {}
  }
});

2.4.3 运行时默认的优先级任务

  1. initializing: 初始化方法(检查当前项目状态,获取配置,等等)
  2. prompting: 与用户进行propt方式交互的的方法,在这个方法中可以调用this.prompt(), 下面的交互中会提及。
  3. configuring: 保存配置并配置项目(创建.editorconfig文件和其他元数据文件)
  4. default: :如果方法名与优先级不匹配,它将被推到这个组。
  5. writing: 在这里编写生成器的特定文件(路由、控制器等). 由此可以知道上面内容中的writing方法也不是乱写的😂😂
  6. conflicts: 处理冲突的地方(内部使用)
  7. install: 安装运行的位置(npm, bower)
  8. end: 最后的调用,可以进行清理,输出结束语等。

2.4.4 异步任务介绍

Yeoman中也支持异步任务,执行异步任务也会暂停run loop,直到异步任务结束后再继续进行。

异步任务可以通过promise的方式实现,如果promise成功则任务圆满结束,如果失败则任务也失败。

// 成功的任务
asyncTask() {
  return Promise.resolve(1)
}
// 失败的任务
asyncTask() {
  return Promise.reject(new Error('err'))
}

除此之外还可以借助this.async()生成回调,在任务结束时候执行回调。

asyncTask() {
  var done = this.async();

  getUserEmail(function (err, name) {
    if(err) {
      done(err); // 失败回调
    }
    done(); // 成功回调
  });
}

2.5 命令行与用户交互

Yeoman提供了命令行中与用户交互的能力,在Yeoman中需要避免使用console.log()process.stdout.write()进行内容的输出,这样可能会造成用户无法再在命令行进行输入的问题,导致无法进行继续交互。替换方法为this.log(),this是当前generator的上下文。

2.5.1 Prompts类型交互

Prompts是generator与用户交互的主要方式,它是由Inquirer.js提供

在generator中可以使用this.prompt()方法进行使用,prompt方法是异步方法,返回结果为promise,因此需要注意使用await等待结果后方可进行后续输出,避免同步代码预先执行。

通过2.4中的内容介绍,我们可以知道,prompts类型的交互需要写在prompting方法中,它也是个异步任务。

使用示例:

const Generator = require('yeoman-generator');

module.exports = class extends Generator {
  async prompting() {
    const 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() {
    ...
  }
}

运行yo example结果:

2.5.2 使用用户交互中的回答

我们可以将用户的回答缓存在实例属性中,之后在其他任务中(如writing任务)使用。

例:

const Generator = require('yeoman-generator');

module.exports = class extends Generator {
  async prompting() {
    // 将用户回答缓存在this.answsers中
    this.answers = await this.prompt([
      {
        type: "input",
        name: "projectName",
        message: "Your project name",
        default: this.appname // Default to current folder name
      },
      {
        type: "input",
        name: "target",
        message: "Say Hello To ",
        default: "Tom"
      },
      {
        type: "confirm",
        name: "cool",
        message: "Would you like to enable the Cool feature?"
      }
    ]);
  
    this.log("app name", this.answers.projectName);
    this.log("cool feature", this.answers.cool);
  }

  writing() {
    this.fs.copyTpl(
      this.templatePath('index.html'),
      // 使用this.answsers中的回答
      this.destinationPath(`${this.answers.projectName}/public/index.html`),
      { title: 'Say Hello', name: this.answers.target }
    );
  }
}

结果:

? Your project name hello
? Say Hello To  Jerry
? Would you like to enable the Cool feature? Yes
app name hello
cool feature true
   create hello/public/index.html

可以成功使用用户的答案。

2.5.3 缓存用户回答

如果创建的generator某些问题用户每次都需要输入相同的内容,则可以将用户的回答进行缓存,以便下次不用输入直接使用。

由于yeoman继承了Inquirer.js,其中用户回答可以通过store属性确定是否进行存储,将其作为未来该问题的默认答案。

使用方式:

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

2.5.4 插播: Yeoman中的缓存机制

以上我们可以看到Yeoman可以缓存用户的回答,那它是怎么做到的呢?

Yeoman中缓存用户的回答后也会将其分享给sub-generators,官网描述,这是一个通用的任务common task.

这些用户的回答最终会被存储在.yo-rc.json文件中。

例:这里将上面sayHello的对象问题缓存了下来

{
  "generator-sample": {
    "promptValues": {
      "target": "Jerry"
    }
  }
}

缓存的这些内容,还提供了相应的api方法供我们去操作他们,以使我们能做出更加灵活的脚手架。

操作API列表: 详细描述见官网文档

  • this.config.save(): 将配置信息保存到.yo-rc.json,如果没有该文件则会进行创建
  • this.config.set(): 可以接受key 和相关的 value,或者多个键值对的对象,但值必须是可以被JSON序列化的(字符串、数字或非递归对象)。
  • this.config.get(key):返回对应的值
  • this.config.getAll(): 返回所有可用的配置项
  • this.config.delete(key): 删除对应key
  • this.config.defaults(defaultValue): 设置默认值,如果对应的key有值,则保持不变,如果key缺省,则添加key与默认value

.yo-rc.json 的数据格式:

{
  "generator-backbone": {
    "requirejs": true,
    "coffee": true
  },
  "generator-gruntfile": {
    "compass": false
  }
}

可以看到每个generator都会有一个自己的命名空间,其中各自的配置项都保存在自己的命名空间中,不会与其他的generator共享。因此上述提及的store保存的内容只能在generator于自己的sub-generator之间共享,不能在多个generator之间共享。

我们也可以预先在该文件中保存默认的配置,同时用户也可以通过该配置文件去修改相关配置。

2.5.5 命令行参数Arguments

我们可以通过yo sample --help查看一些帮助信息,其中提供了一些参数的用法,我们也可以添加自定义的参数。

注意:

yo sample中的sample是上面我们自己写的脚手架的名字。

yo sample my-project

其中my-project就是第一个参数

例:

	constructor(args, opts) {
    super(args, opts);
    // 参数1
    this.argument("test1", { type: String, required: true, desc: "this is an test1 argument", default: 'hello' });
    // 参数2
    this.argument("test2", { type: Number, required: true, desc: "this is an test2 argument", default: 100 });

    this.log(this.options.test1);
    this.log(this.options.test2);
  }

在generator的构造函数constructor中,可以根据this.argument(paramName, paramConfig)进行参数的配置,参数的顺序就是yo sample param1 param2从param1开始依次去匹配this.argument设置的顺序。

this.argument第一个形参为参数名称,第二个形参为参数的信息,其包含四个选项:

  • desc: 参数的描述信息
  • required:Boolean类型,参数是否必须
  • type:参数类型,可以为String, Number, Array ,也可以是一个自定义函数接收行内字符串值并解析它
  • default: 参数默认值

以上设置的结果,通过yo sample --help可以看到:

hello # 通过this.log打印的第一个值
100 # 通过this.log打印的第二个值
Usage:
  yo sample:app <test1> <test2> [options]

Options:
  -h,   --help           # Print the generator's options and usage
        --skip-cache     # Do not remember prompt answers               Default: false
        --skip-install   # Do not automatically install dependencies    Default: false
        --force-install  # Fail on install dependencies error           Default: false
        --ask-answered   # Show prompts for already configured options  Default: false

Arguments:
  test1  # this is an test1 argument  Type: String  Required: true
  test2  # this is an test2 argument  Type: Number  Required: true

2.5.6 Options选项

上方通过yo sample --help中的--help就是一种选项,它与参数相似但书写格式略有不同。

它的定义与使用也与参数差不多,参数的定义用了this.argument(),options需要使用this.option(optionName, optionConfig)

this.option的第一个参数为选项的名称,--help的名称就是help,第二个参数包含五个选项:

  • desc: 选项的描述信息
  • alias: 选项民惠城更的短格式,例如--help的段格式为-h
  • type:选项类型,可以为String, Number, Array ,也可以是一个自定义函数接收行内字符串值并解析它
  • default: 默认值
  • hide: Boolean类型,是否隐藏帮助信息

示例:

constructor(args, opts) {
    super(args, opts);
    ...
    this.option("coffee", {
      desc: '是否使用CoffeeJs',
      alias: 'c',
      type: Boolean,
      default: false,
      hide: false
    });

    this.option("typescript", {
      desc: '是否使用typescript',
      alias: 'ts',
      type: Boolean,
      default: false,
      hide: false
    });

    this.log(this.options.coffee);
    this.log(this.options.typescript);
  }

以上设置的结果,通过yo sample --help可以看到:

false # 通过this.log打印的coffee的值
false # 通过this.log打印的typescript的值
Usage:
  yo sample:app <test1> <test2> [options]

Options:
  -h,    --help           # Print the generator's options and usage
         --skip-cache     # Do not remember prompt answers               Default: false
         --skip-install   # Do not automatically install dependencies    Default: false
         --force-install  # Fail on install dependencies error           Default: false
         --ask-answered   # Show prompts for already configured options  Default: false
  -c,    --coffee         # 是否使用CoffeeJs                                 Default: false
  -ts,   --typescript     # 是否使用typescript                               Default: false

Arguments:
  test1  # this is an test1 argument  Type: String  Required: true
  test2  # this is an test2 argument  Type: Number  Required: true

注意:

通过以上使用,我们发现,无论是参数还是选项,我们都可以通过this.options.name的方式获取到相应的值,从而进行判断是否需要做其他操作。

2.6 通过流stream输出文件

Yeoman中的Generator允许我们对每个文件的写入进行拦截过滤,并可以使用自定义的过滤方式。例如对代码进行美化,对空格进行格式化等操作。

我们可以通过注册transformStream对文件或文件路径等的修改,相应的api为registerTransformStream()

例:

// Yeoman官方示例代码
var beautify = require("gulp-beautify");
this.registerTransformStream(beautify({ indent_size: 2 }));

由于Yeoman使用了gulp一致的vinyl处理对象流,因此很多gulp的插件可以在Yeoman中进行使用。

不过需要注意的是,每个文件的写入都会通过以上注册的stream进行,因此,可以采用 gulp-if or gulp-filter 进行类型判断。

2.7 小结

以上介绍了一些Yeoman中的常用内容,关于Yeoman的更深入使用,请查看官方文档

3. Plop.js

3.1 plop.js介绍

官网

对于plop.js,官方的描述为:Plop is a little tool that saves you time and helps your team build new files with consistency.(Plop是一个小工具,可以节省您的时间,并帮助您的团队构建具有一致性的新文件).

相比于Yeoman这种大型的脚手架,plop.js确实比较小型,通常用于在项目中帮我们创建一些包含通用代码的routes, controllers, components等类型的代码片段。(在vue-element-admin项目中我们也可以看到plop的身影,笔者第一次接触它也是看vue-element-admin源码时候发现的,然后就将其应用在了自己的项目中)

plop.js也是通过 inquirer.js实现命令行与用户进行交互,模板文件采用了handlebars 模板引擎。

3.2 plop.js的安装与基本使用

1. 安装:

yarn add plop -D

or

npm install --save-dev plop

2. 在项目根目录创建 plopfile.js文件

plopfile.js需导出一个函数,该函数接收一个plop参数,plop对象暴露了plop相关api,其中包含的setGenerator(name, config)方法可以创建一个generator,当然它也可以多次使用,创建多个generator。

setGenerator第一个代表generator的名称,第二个参数为配置项,包含:

  • description: String, generator所做事情的描述
  • prompts: Array<InquirerQuestion>InquirerQuestion,由于plop也是基于inquirer.js实现的命令行交互,因此这里可以参考inquirer.js的数据结构,其与Yeoman基本一致
  • actions: Array<ActionConfig>动作的执行,点击查看ActionConfig数据结构

例:

module.exports = function (plop) {
    // 创建generator
    plop.setGenerator('component', {
        description: '创建react组件',
        prompts: [], // array of inquirer prompts
        actions: []  // array of actions
    });
};

3. 创建模板文件

polp的模板一般放在plop-templates目录中,模板语法为handlebars,可以点击链接查看其基本语法

// your-project/plop-templates/component/components.jsx.hbs
import React from 'react';
import './{{componentName}}.css';

function {{componentName}}() {
  return (
    <div className="{{componentName}}">
      
    </div>
  );
}

export default {{componentName}};

// your-project/plop-templates/component/components.css.hbs
.{{componentName}} {
  text-align: center;
}

// your-project/plop-templates/component/components.test.js.hbs
import React from 'react';
import { render } from '@testing-library/react';
import {{componentName}} from './{{componentName}}';

test('renders learn react link', () => {
  const { getByText } = render(<{{componentName}} />);
  const linkElement = getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

4. 完善plopfile.js,完成generator编写

以下actions中共有三个action,且每个type都为add,代表添加文件,path为目标文件,templateFile为模板文件。

module.exports = function(plop) {
  plop.setGenerator('component', {
    description: "创建react组件",
    prompts: [
      {
        type: 'input',
        name: 'componentName',
        message: '请输入组件名称: ',
        default: 'MyComponent'
      }
    ],
    actions: [
      {
        type: 'add',
        path: 'components/{{componentName}}/{{componentName}}.jsx',
        templateFile: 'plop-templates/component/component.jsx.hbs'
      },
      {
        type: 'add',
        path: 'components/{{componentName}}/{{componentName}}.css',
        templateFile: 'plop-templates/component/component.css.hbs'
      },
      {
        type: 'add',
        path: 'components/{{componentName}}/{{componentName}}.test.js',
        templateFile: 'plop-templates/component/component.test.js.hbs'
      }
    ]
  })
}

5. 运行yarn plopornpx plop使用generator

结果为:

? 请输入组件名称:  HelloWorld
✔  ++ /components/HelloWorld/HelloWorld.jsx
✔  ++ /components/HelloWorld/HelloWorld.css
✔  ++ /components/HelloWorld/HelloWorld.test.js
✨  Done in 18.45s.

就可以轻松创建基础的组件。如果需要创建其他类型的基础代码文件,例如路由、store、controller等可以继续 plop.setGenerator继续编写需要的generator并准备好相关的模板文件。

3.3 小结

本节介绍了plop.js创建基础代码文件的例子,plop.js是一个比较简单的小工具,以上介绍已可以满足基本的需求,如需了解其他api可以到plop.js官网进行深入了解。

4. 窥探脚手架原理,使用Node原生实现

4.1 node cli应用

脚手架本身就是一个node的cli应用,我们可以通过node cli应用来实现自己的脚手架。

通过以上对Yeoman与Plop.js的介绍,我们可以发现它们在实现时都使用了 inquirer.js 与用户进行交互。

使用node cli应用实现脚手架,我们可以选择inquirer.js结合结合commander.js,并搭配各种小工具例如chalk.js, log-symbols等小工具自定义一些输出信息。

node cli应用的核心就是在package.json中添加一个"bin"字段,指向js cli脚本,js脚本的书写方式必须由以下代码开头,其他代码则为正常的js代码

#!/usr/bin/env node
console.log('hello')

4.2 通过node实现脚手架的方案

  • 创建一个项目xxx-cliyarn init初始化项目

  • 添加bin/cli.js文件,并填写如下代码

    #!/usr/bin/env node
    console.log('hello world')
    
  • package.json中添加bin字段

    {
      "name": "node-cli",
      "version": "1.0.0",
      "main": "index.js",
      "bin": {
        "node-cli": "./bin/cli.js"
      },
      "license": "MIT",
      "dependencies": {
        "ejs": "^3.1.5",
        "inquirer": "^7.3.3"
      }
    }
    
  • 在项目根目录通过npm link或者yarn link创建全局软连接

  • 如果在mac或linux下编写代码还需要给cli脚本添加执行权限chmod 755 ./bin/cli.js

  • 然后随便进入一个目录,执行node-cli,注意你自己在bin字段下填写的key是什么,命令就是什么,这里是node-cli,然后输出hello world

至此使用node实现了cli的应用,脚手架所需的基本知识已经具备。

除此之外,脚手架本质上就是通过cli与用户进行各种交互,收集回答后根据用户回答选择性地创建项目模板。所以再结合 inquirer.jscommander.js 可以进行与用户的交互,并收集回答。

结合 ejs template syntax 等模板方案,根据收集的回答,为用户生成相应的基础代码。