Talk is cheap. Show me your code

从零开始搭建前端脚手架

一、功能设计

每个前端小组都会有自己的独特的业务场景,从这些业务场景从提取公共部分,并打造一个前端项目模版,是非常有必要的

为了能够基于这个项目模版快速创建一个新项目,就需要脚手架工具登场

所以这里至少有两个项目仓库:前端模版项目、脚手架工具

而对于脚手架工具,它应当具备这样的功能:输入一个命令和项目名称,创建对应的项目目录,其内容就是模版项目

my-cli create my-project

基于此,脚手架工具的内部逻辑也就很清晰了:

创建一个 create 命令,其行为是拉取模版项目的代码(用 git clone 即可实现)

为了实现这个命令,我们需要借助一个神奇的工具:commander

 

 

二、Commander

Commander.js 是完整的 node.js 命令行解决方案

可以通过它创建一个 program 对象,作为程序的主体

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

commander 常用的功能有创建选项和命令


选项 option 需要基于短线 " - " 声明,常见的  -h 、 -V  都是选项

每个选项可以定义一个短选项名称(" - " 后面接单个字符)和一个长选项名称(" -- "后面接一个或多个单词)

解析后的选项可以通过 .opts() 方法获取,从而执行对应的操作

比如创建一个 -d 选项:

program
  .option('-d, --debug', 'output extra debugging')

program.parse(process.argv);

const options = program.opts();
if (options.debug) console.log(options);

还可以对选项定义参数及默认值:

program
  .option('-d, --debug <type>', 'output extra debugging', 'hard')

这里就对 debug 选项定义了 type 参数,并设置其默认值为 hard

用尖括号   <>  定义的参数为必填,用中括号   []  创建的参数为选填


命令 command 可以通过  .command()  创建,并通过链式调用 .description() 和 .action() 定义该命令的描述和具体行为

program
  .command('create <name> [type]')
  .description('create a new project')
  .action((name, type) => {
    console.info('project', name);
  });

和选项类似,命令也可以通过  <>  和  []  定义参数

除了像上面那样通过链式调用创建命令行为(description、action)之外,还可以用单文件的形式描述命令,不过我没有跑通


commander 还有更多更强大的功能,这里就不逐一介绍了,详情可以参考中文文档

 

 

三、正式开始

学习了 commander 之后,就可以着手脚手架的开发了 

首先创建脚手架工具的目录,如 my-cli,然后在目录下创建一个 .gitignore 文件

# Dependency directories
node_modules/

接着通过  npm init 创建 package.json

创建之后可以删除其中的 main 和 scripts,然后安装 commander

npm install commander --save

然后创建 bin 目录及 bin/cli.js 文件:

#!/usr/bin/env node

const { Command } = require('commander');
const { name, version } = require('../package.json');
const program = new Command();

program.name(name).version(version);

program
  .command("create <project-name>")
  .description("create a new project")
  .action((name) => {
    console.info('project', name);
  });

program.parse();

顶部的  #!/usr/bin/env node  是告诉操作系统,用 /usr/bin 下的 node 来执行这个脚本

 

一个简单的 cli 命令就创建好了,可以回到根目录,用 node 执行 cli.js 试试:

node bin/cli.js create my-project

可以看到 create 命令正常执行 

但直接通过 node + 路径 的形式运行代码还是太死板了,可以通过  npm link 将项目挂到全局,就能像正常的脚手架工具那样用了

首先需要在 package.json 里添加 bin 命令:

{
  ...
  "bin": {
    "my-cli": "bin/cli.js"
  }
}

这里的 my-cli 是包的名称,后面的路径需要写全后缀

然后在根目录执行  npm link 就能将 my-cli 链接到全局

npm link 是一个很好的本地测试的手段,调试完成后可以通过 npm unlink 卸载

 

 

四、完善 create 命令 

据文档介绍, commander 支持将命令拆成单文件进行维护,但我没有搞出来,最后只好加了一层 map 来拆文件

在根目录下新建一个 command 目录,并创建 command/create.js 和 command/index.js 两个文件:

然后将 create 命令的相关逻辑移到 command/create.js 中

// create.js

function createAction(name) {
  console.log('project', name);
}

const create = {
  alias: 'c',
  params: '<project-name>',
  description: 'create a new project',
  action: createAction,
}

module.exports = create;

并在 command/index.js 中导出

// index.js

const create = require('./create.js');

module.exports = {
  create,
};

最后来改造 bin/cli.js

#!/usr/bin/env node

const { Command } = require('commander');
const { name, version } = require('../package.json');
const commands = require('../command/index.js');
const program = new Command();

program.name(name).version(version);

// 创建命令
Reflect.ownKeys(commands).map((name) => {
  const { params, alias, action, description } = commands[name] || {};
  program.command(`${name} ${params || ''}`) 
    .alias(alias) 
    .description(description) 
    .action((...args) => {
      typeof action === 'function' && action(...args);
    })
});

program.parse(process.argv);

项目的结构基本成型,接下来完善 create 命令

在文章的开头就已经分析过了,create 命令只需要做一个事情,就是将项目 clone 到当前目录

const { exec } = require("child_process");

function createAction(name) {
  // 这是模板项目的仓库地址
  const url = "http://github.com/xxx/template.git";
  // 克隆项目
  exec(`git clone ${url} ${name}`, (error, stdout, stderr) => {
    if (error) {
      console.log(error);
      process.exit();
    }
    console.log("Success");
    process.exit();
  });
}

如果模板项目是 github 上的项目,应该没什么大问题

而如果模板项目是小组内部的 gitlab 项目,就需要放开模板项目的访问权限,至少让小组人员都能够访问

 

 

五、优化体验

在完成了 create 命令之后,我们的脚手架工具就可以算是开发完成

但为了更好的体验,可以借助这些工具加以改造:chalkinquirer

 

1. chalk

它可以在命令行打印彩色文字:

import chalk from 'chalk';

const error = chalk.bold.red;
const warning = chalk.hex('#FFA500');

console.log(chalk.blue('Hello world!'));
console.log(error('Error!'));
console.log(warning('Warning!'));

 

2. inquirer

这是一个让用户与命令行交互的工具

它提供了很多 api,让用户可以在程序运行的过程中输入内容,从而影响程序运行的结果

const inquirer = require('inquirer');

inquirer
  .prompt([
    /* Pass your questions in here */
  ])
  .then((answers) => {
    console.log('success', answers);
  })
  .catch((error) => {
    console.log('error');
  });

在上面的 prompt 中配置需要用户输入/选择的(表单)内容

比如让用户输入项目名称:

.prompt([
  {
    type: "input",
    message: "项目名称:",
    name: "projectName",
    validate: (val) => {
      // 对输入的值做判断
      if (!val || !val.trim()) {
        return chalk.red("项目名不能为空,请重新输入");
      } else if (val.includes(" ")) {
        return chalk.red("项目名不能包含空格,请重新输入");
      }
      return true;
    },
  },
])

除了这里的 input 类型外,inquirer 还提供了很多交互类型,如单选列表 list、多选 checkbox、确认项 confirm 等

 


在了解了 chalk 和 inquirer 之后,我们就可以进一步改造 create 命令

npm install inquirer chalk --save
// create.js
const inquirer = require("inquirer");
const chalk = require("chalk");
const { exec } = require("child_process");

function createProject(name) {
  // 这是模板项目的仓库地址
  const url = "git@github.com:wisewrong/chart-admin.git";
  exec(`git clone ${url} ${name}`, (error, stdout, stderr) => {
    if (error) {
      console.log(chalk.red(error));
      process.exit();
    }
    console.log(chalk.green("Success"));
    process.exit();
  });
}

const create = {
  alias: "c",
  params: "[project-name]",
  description: "create a new project",
  action: (project) => {
    project
      ? createProject(project)
      : inquirer
          .prompt([
            {
              type: "input",
              message: "项目名称:",
              name: "projectName",
              validate: (val) => {
                // 对输入的值做判断
                if (!val || !val.trim()) {
                  return chalk.red("项目名不能为空,请重新输入");
                } else if (val.includes(" ")) {
                  return chalk.red("项目名不能包含空格,请重新输入");
                }
                return true;
              },
            },
          ])
          .then((answer) => {
            createProject(answer.projectName);
          });
  },
};

module.exports = create;

麻雀虽小五脏俱全,这样一个简单的脚手架就开发完了

如果需要发布到 npm,可以参考我之前的文章《vue-cli 3.x 开发插件并发布到 npm》

后面我也会继续完善这个脚手架,添加更多的交互,以及展示进度条(专业挖坑,从来不填...)

posted @ 2021-08-03 10:38  Wise.Wrong  阅读(2543)  评论(2编辑  收藏  举报