上文 从头开始构建你的开发生态系统:自定义脚手架的完整指南(一)已经讲解了cli相关的知识, 本文就着重讲解一下实操,手把手实现一个自己的cli脚手架,由于上篇文章已经对cli相关知识点讲解过了,本文就直接上代码了,不会太多原理解析
废话不多说,直接上干货.
准备工作
- 先初始化一个项目
- 然后新建bin目录,新增index.js文件
- 在
package.json中新增bin配置
cd demo-cli
npm init -y
当前目录结构
bin
└─ index.js
package.json
package.json
{
"name": "cli-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": {
"cli": "./bin/index.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
ps: bin中的那个key看自己喜好定义,可定义多个,在本文中定义为cli;
这个bin/index.js文件就是触发cli全局变量时会执行的文件, 添加简单代码
#!/usr/bin/env node
console.log('我是cli---');
然后执行npm link
然后进行测试,在控制台输入cli 回车,就会发现控制台会输出'我是cli---'
到这里 恭喜你,以及拥有一个属于自己的脚手架了, 哈哈
开个玩笑,继续上菜
解析命令行参数
当我们使用vue-cli的时候,会经常使用如下指令来创建一个项目,
vue create project-name
// or
vue create project-name --force
那么该怎么定义create project-name 或者 --force这些指令? 该怎么解析这些参数呢?
上面的准备工作中,我们已经拥有了cli这个全局指令, 那么我如果输入cli create name, 我怎么获取到这些cli后面的参数呢?
这里先采用原生的方法,process.argv 属性返回一个数组,这个数组包含了启动Node.js进程时的命令行参数, 我们现在bin/index.js中新增一行代码
#!/usr/bin/env node
let argv = process.argv
console.log(argv);
然后控制台输入 cli create name,回车后看控制台可以看到
可以看到,输入的参数是存在在数组的第二位及以后的,也就是说
let argv = process.argv.slice(2) // log [ 'create', 'name' ]
但是这样做,对参数的解析,校验要写很多的逻辑,有没有一个工具能让我们自定义指令和校验参数呢,答案是当然有的,commander 编写代码来描述你的命令行界面。 Commander 负责将参数解析为选项和命令参数,为问题显示使用错误,并实现一个有帮助的系统。
Commander 是严格的,并且会针对无法识别的选项显示错误。 两种最常用的选项类型是布尔选项,和从参数中获取值的选项。
然后让我们安装它
npm install commander -D
然后在bin/index.js中写入
#!/usr/bin/env node
const { Command } = require('commander')
const chalk = require('chalk')
const program = new Command()
const { version, name } = require('../package.json')
const log = console.log;
program.version(version).usage("command <options>");
program.on("--help", () => {
log(`Run ${chalk.red(`${name} <command> --help`)} show details`);
});
program
.command("create <project-name>")
.description("创建一个项目")
.option("-f --force", "overwrite target if it exists")
.action((name, options) => {
if (!name) {
console.log(chalk.red('请输入项目名称'));
return
}
console.log(options);
log('执行create操作')
});
program
.command("add")
.description("add a new template")
.action(() => {
log('执行add操作')
});
program
.command("list")
.description("see template list")
.action(() => {
log('执行list操作')
});
program.parse(process.argv);
if (!program.args.length) {
program.help();
}
看到引用的包还会发现存在一个chalk的包
chalk 控制log在控制台输出的颜色
- 控制log在控制台输出的颜色
"chalk": "2.4.2"新版的是采用的ESM模式,如果在node中使用,就安装开头的这个版本
回归主线,看到上述代码. 表示注册了四个指令以及help帮助提示, 控制台输入如下
然后我们可以针对不同的指令编写不同的代码, 以add为例,为了代码好维护, add指令相关的代码会抽取到单独的文件去编写
// bin/index
program
.command("add")
.description("add a new template")
.action(() => {
require('../libs/command/add')
});
libs/command/add
#!/usr/bin/env node
// 交互式命令行
const inquirer = require("inquirer");
// 修改控制台字符串样式
const chalk = require("chalk");
const fs = require("fs");
// 读取根目录下的template.json
const tplObj = require(`${__dirname}/../../template`);
// 自定义交互式命令行的问题和简单校验
let question = [
{
name: "name",
type: "input",
message: "请输入模板名称",
validate(val) {
if (val === "") {
return "请输入模板名称";
} else if (tplObj[val]) {
return "模板已存在!";
} else {
return true;
}
},
},
{
name: "type",
type: "rawlist",
message: "请选择模板仓库类型",
choices: [
{
name: "github:",
},
{
name: "gitlab:",
},
{
name: "Bitbucket:",
},
],
},
{
name: "url",
type: "input",
message: "请输入模板地址",
validate(val) {
if (val === "") return "请输入模板地址";
return true;
},
},
];
inquirer.prompt(question).then((options) => {
// options是用户输入的参数 是一个对象
let { name, url, type } = options;
// 过滤 unicode 字符
tplObj[name] = type + url.replace(/[\u0000-\u0019]/g, "");
// 把模板信息写入template.json文件中
fs.writeFile(
`${__dirname}/../../template.json`,
JSON.stringify(tplObj),
"utf-8",
(error) => {
if (error) console.log(error);
console.log("\n");
console.log(chalk.green("Added successfully!\n"));
console.log(chalk.grey("The latest template list is: \n"));
console.log(tplObj);
console.log("\n");
}
);
});
上述add操作其实就是维护了一个模板列表, 进行新增和删除操作,提供多重模板, 还有另一个做法是采用git提供的接口, 把模板放在一个仓库中, 调用接口返回模板信息, 但我个人觉得自己维护一个template.json比较灵活,可以突破仓库的限制
create创建操作,可以简单的分为下面几步(更严谨的话步骤更多)
- 获取当前工作目录
- 判断当前目录下是否存在当前操作的目录名字
- 让用户选择要使用的模板
- 拉取当前组织下的模版
- 下载当前的模版所需依赖
- 给出相关成功提示
下面是部分代码示例, 后面会给出完整的
上面两步骤,一个增加模板,一个使用, 核心流程是完成的, 删除和list 其实就是对模板数据的操作, 这里不展示了. 其实到这里,一个简单版的cli就完成了
注意: 下载所需依赖的时候注意版本, 部分依赖的新版本号变成esm格式了,大家注意一下
仓库地址: 仓库地址