如何从0搭建一个CLI脚手架

719 阅读5分钟

一、CLI有啥用,认识CLI

前端开发过程中常见的CLI有:

  • create-react-app
  • vue-cli
  • webpack-cli
  • prettier-cli

基本复杂一点的工具都在集成CLI,为啥都要搞成CLI呢?

因为CLI可以提供更强大的功能:

  • 通过命令搭配实现不同的功能
  • 管理项目模版
  • 启动本地服务
  • 生成模版文件
  • 对代码进行格式化

二、搭建一个最简单的CLI

我们先搭建一个最简单的CLI来体验下,然后逐步实现复杂点的功能。

1、新建项目

首先新建项目作为CLI源代码地址

mkdir cli-demo
cd cli-demo
npm init

2、安装依赖

yargs文档配置:该插件将用户通过终端输入的参数解析成对象,可以配置各种参数。

yarn add yargs

命令使用语法:

.command(cmd, desc, [builder], [handler])

3、CLI命令配置

下面的配置说明:

  • 配置命令get
  • 声明信息make a get HTTP request
  • 并配置了参数url信息
  • 最后callback函数获取用户执行的参数

具体使用:编辑index.js,第一行一定要加注释,表明运行在node环境下

#!/usr/bin/env node

const yargs = require('yargs/yargs')
const { hideBin } = require('yargs/helpers')

yargs(hideBin(process.argv))
  .command(
    'get',
    'make a get HTTP request',
    function (yargs) {
      return yargs.options({
        url: {
            alias: 'u',
            describe: 'the URL to make an HTTP request to'
        }
      })
    },
    function (argv) {
      console.log("callback", argv.url)
    }
  )
  .help()
  .argv

执行命令,可以获取输入的参数

image.png

这里get就是定义的指令,url是指令下的key值,用--url输入,alias就是别名,用-u表示,后面跟要输入的参数。

3、npm发布

接着我们发布下npm,然后一个CLI就完成了。

登陆npm仓库,没有的话去注册一个,地址

npm login

选择一个中意的cli名字,查一下是否存在,这里我们起个名字cli-demo3

image.png

执行npm version patch && npm publish --registry=https://registry.npmjs.org,不出意外的话,就发布成功了。

image.png

4、下载使用

然后我们本地使用下,首先下载到本地,全局下载npm install -g cli-demo3

image.png

cli-demo3是我们前期测试使用写的CLI,后面继续使用我正常部署的CLI:u-amin-cli,原理是一样的

三、终端交互优化

1、log-symbols:输出结果用图标标识

# 这里使用4版本,升级到5版本不再支持require方式导入
yarn add log-symbols@4.1.0

使用方法

image.png

使用效果

image.png

2、chalk:打印出各种颜色的文案提示,功能强大,几乎支持所有的颜色

# 这里使用4版本,升级到5版本不再支持require方式导入
yarn add chalk@4.1.2

1、支持传入string,array和模版字符串

使用方法

console.log(chalk.blue("this is blue"))

console.log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));

console.log(`
CPU: ${chalk.red('90%')}
RAM: ${chalk.green('40%')}
DISK: ${chalk.yellow('70%')}
`);

使用效果

image.png

2、支持修改背景色、各种颜色格式、指定不同文本的颜色

使用方法

console.log(chalk.blue.bgRed.bold('Hello world!'));

console.log(chalk.red('Hello', 
chalk.underline.yellowBright('world') + '!'));

console.log(chalk.hex('#DEADED').underline('Hello, world!'))

console.log(chalk.rgb(15, 100, 204).inverse('Hello!'))

使用效果

image.png

3、支持自行封装一些颜色

使用方法

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

console.log(error('Error!'));
console.log(warning('Warning!'));

使用效果

image.png

3、ora:增加loading效果

# 这里使用5版本,升级到6版本不再支持require方式导入
yarn add ora@5.4.1

使用方法

const ora = require('ora');
 
const spinner = ora('Loading unicorns').start();
 
setTimeout(() => {
    spinner.color = 'yellow';
    spinner.text = 'Loading rainbows';
}, 1000);

使用效果

image.png

4、inquirer:在终端选择命令

使用方法

var inquirer = require('inquirer');
inquirer
  .prompt([
    {
      name: "templateType",
      type: "list",
      default: "vue",
      choices: [
        {
          name: "React",
          value: "react",
        },
      ],
      message: "Select the template type.",
    }
  ])
  .then((answers) => {
    // Use user feedback for... whatever!!
  })
  .catch((error) => {
    if (error.isTtyError) {
      // Prompt couldn't be rendered in the current environment
    } else {
      // Something else went wrong
    }
});

使用效果

image.png

这样就可以给客户提示选择来处理逻辑。

5、download-git-repo:download Git仓库

使用方法

const download = require("download-git-repo");

download('direct:https://gitlab.com/flippidippi/download-git-repo-fixture.git#my-branch''test/tmp', { clonetrue }, function (err) {
  console.log(err ? 'Error' : 'Success')
})

还有很多类似的插件,来丰富cli的功能,根据需要添加即可

  • open 打开浏览器
  • yargs/commander 执行cli命令
  • get-port 获取当前端口号

四、CLI优化

1、提取命令行配置到一个文件中

启动文件index.js

const yargs = require('yargs/yargs')
const { hideBin } = require('yargs/helpers')
const config = require('./config')

const yargsCommand = yargs(hideBin(process.argv))

config.forEach(commandConfig => {
  const { command, descriptions, options, callback } = commandConfig
  yargsCommand.command(
    command,
    descriptions,
    yargs => yargs.options(options),
    (argv, ...rest) => {
      callback(argv, ...rest);
    }
  )
})
  
yargsCommand.help().argv

指令文件config.js

const commandOptions = [
  {
    command: "create",
    descriptions: "拉取一个项目模版",
    options: {
      name: {
        alias: "n",
        type: "string",
        require: true,
        describe: "项目名称",
      },
    },
    callback: async (argv) => {
      create({name: argv.name})
    }
}]

提取配置,就很清晰了

2、配置ESLint和Prettier,之前专门写了自动化检查代码的文章。

前端工程化:Prettier+ESLint+lint-staged+commitlint+Hooks+CI 自动化配置处理

3、提取脚本生成使用文档

上一步我们对配置进行了提取,接着根据配置生成使用文档,如下

image.png

使用方法

yarn add json2md@1.12.0 -D
const json2md = require("json2md")

// 按json2md需要的数据格式组合
const data = json2md([
    { h1: "JSON To Markdown" }
  , { blockquote: "A JSON to Markdown converter." }
]))

// 写入Readme.md文档
fs.writeFile(path.join(__dirname, "../Readme.md"), json2md(data), (err) => {
  if (err) throw err;
  console.log("update docs success");
});

五、CLI管理模版项目

1、配置下载命令

{
    command: "create",
    descriptions: "拉取一个项目模版",
    options: {
      name: {
        alias: "n",
        type: "string",
        require: true,
        describe: "项目名称",
      },
    },
    callback: async (argv) => {
      create({name: argv.name})
    }
}

2、准备模版文件列表

const map = {
   vue: "https://github.com/luchx/ECHI_VUE_CLI3.0.git",
   react: "git@github.com:richLpf/antd-template-demo.git#main"
}

3、下载逻辑

1、inquirer提示用户选择对应的模版,具体可以看前面用法

2、通过download下载模版文件

module.exports = function downloadFromRemote(url, name) {
  return new Promise((resolve, reject) => {
    download(`direct:${url}`, name, { clone: true }, function (err) {
      if (err) {
        reject(err);
        return;
      }
      resolve();
    });
  });
};

3、通过ora在下载过程中展示loading

image.png

这是主要的下载逻辑,关于日志,我们可以更好的展示,也可以配置更复杂的命令:替换下载的项目名称,删除.git目录,修改package.json配置

六、通过CLI启动API模拟接口服务

前端在开发的过程中,总会遇到后端接口无法按时提供的情况,这种情况下有很多解决方案,常见的通过客户端生成模拟api,在项目中引入Mock。

这里我们通过CLI来实现一个更加简单好用的模拟方案

1、方案原理

通过CLI启动一个或多个服务器,通过Express做路由转发本地的JSON文件

2、命令配置

首先我们配置mock命令,设定参数typeportcreate分别代表API类型、启动Http服务器端口号,默认9000,create是否自动生成接口数据JSON文件。

{
    command: "mock",
    descriptions: "启动一个本地服务,模拟返回接口数据",
    options: {
      type: {
        alias: "t",
        type: "string",
        default: "action",
        describe: "选择API类型",
        choices: ['action', 'restful']
      },
      port: {
        alias: "P",
        type: "number",
        default: 9000,
        describe: "选择启动的端口号",
      },
      create: {
        alias: "c",
        type: "boolean",
        default: false,
        describe: "如果mock目录不存在是否自动创建,默认不自动创建"
      },
    },
    callback: async (argv) => {
      mock({
        ...argv
      })
    }

3、Http服务处理

1、启动express服务器,配置参数解析和跨域设置,并获取mock数据的目录,启动Http服务器

const app = express()
app.use(bodyParser.json({limit: '50mb'}))
app.use(express.urlencoded({ extended: true }));

app.all('*', (req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*'); //访问控制允许来源:所有
    res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); //访问控制允许报头 X-Requested-With: xhr请求
    res.header('Access-Control-Allow-Metheds', 'PUT, POST, GET, DELETE, OPTIONS'); //访问控制允许方法
    res.header('X-Powered-By', 'nodejs'); //自定义头信息,表示服务端用nodejs
    res.header('Content-Type', 'application/json;charset=utf-8');
    next();
});

const filePath = path.join(process.cwd(), `./mock/`)

app.listen(port, () => console.log(`Mock api listening on port ${port}!`));

2、如果是Action类型的API

比如请求url为:

http://localhost:9000/acl

请求体为:

{
    Action: "GetUsers"
}

此时在CLI执行的目录,新建mock/acl/GetUsers.json

GetUsers.json内容为返回的json数据

{
    "RetCode": 0,
    "Message": "this is error",
    "Data": []
}

然后通过fs模块读取json文件的数据,直接返回模拟的数据,因为是实时读取的,所以更新json数据不需要重启服务。主要逻辑如下

app.post('*', async(req, res) => {
    const key = req.params[0].substring(1)
    const { Action } = req.body
    const file = `${filePath}${key}/${Action}.json`;
    fs.readFile(file, 'utf-8', function(err, data) {
        if (err) {
            res.send(NotFoundResponse);
        } else {
            res.send(data);
        }
    })
})

3、如果是Restful类型的API

比如请求url为:

http://localhost:9000/acl/users

post请求体为:

{
    limit: 10
}

此时在CLI执行的目录,新建mock/acl/users/post.json

post.json内容为返回的json数据

{
    "RetCode": 0,
    "Message": "get users",
    "Data": []
}

如果是get请求,只需要添加get.json就可以了。

然后同样通过fs读取对应的json数据

app.all("*", async(req, res) => {
    const key = req.params[0]
    const method = req.method
    const file =`${filePath}${key}/${method}.json`
    console.log("file", file)
    fs.readFile(file, 'utf-8', function(err, data) {
        if (err) {
            res.send(NotFoundResponse);
        } else {
            res.send(data);
        }
    })
})

这样就完成了CLI启动一个模拟的http服务,用起来很方便,并且可以支持直接在项目中使用。

具体可以试试看u-admin-cli

对于大部分mock来说,复杂的配置是不需要的,这里仅仅是用了固定的json数据,我觉得就够用了,当然还可以自行处理数据,引入Mock规则,自行定义生成数据类型等

以上就是搭建一个CLI的过程,我们从最简单的开始构建了一个基础CLI,在这个基础上进行了交互优化,然后实际开发了CLI的两个功能:

  • 1、通过CLI管理部门内的模版项目
  • 2、通过CLI启动Http服务器,模拟接口生成

项目代码:github.com/richLpf/u-a…

CLI地址:www.npmjs.com/package/u-a…

码字不易,欢迎点赞👍,有问题请留言。