前言
在日常开发中,我们经常会使用到各种脚手架工具(cli): vue-create-app
,ng
包括 npm
。它们极大简化了开发人员对于项目结构和文件创建的工作,让我们可以把精力专心在业务实现上。 对于某些项目而言 cli还可以封装一些脚本,用来处理项目中的一些特殊场景。
开发cli的好处:
- 简化项目初始化流程,对于部门内新立项的项目能快速的生成项目骨架。
- 集成项目通用指令,例如:本地化发布上传,subtree 拉取配置等,部门内所有项目统一升级等。
- 减少重复工作,跟据用户输入还可以进行差异化配置
原理
cli 不论有多少功能基本原理就是利用 nodejs 来进行脚本和文件的各种操作,比如 init
等初始化的指令就是在指定的模板代码仓库中拉取相应的代码,再跟据用户输入进行模板替换。 再比如 npm run serve
这类的指令就是利用node 的环境运行一些 shell脚本达到相应的功能。
准备工作
编写cli能使用到的工具库有很多,并且各有各的优点,最为常见的配套按 使用顺序 如下:
commander.js,解析用户命令(init, create, -v, help 等)
inquirer,执行命令后的用户交互集合有多种可供支持的交互类型(input, checkbox, list 等)
download-git-repo,下载指定仓库指定分支下的代码,作为项目模板
handlebars,模板引擎,用户根据用户输入替换项目模板中的内容实现自定义与差异化
除开以上功能性的模块,还有许多可以用来提高用户体验的包:
ora, 执行命令时的动画效果,用来标识cli的运行进度。
chalk,给通过终端console命令输出的文本添加字体颜色。
log-symbols,给通过终端console命令输出的文本添加符号标识(info, success,warning,error四种)
步骤
想直接查看源码的请拖到页面底部。
初始化项目
创建一个空目录,根据你的喜好命名(示例项目为d-cli),然后进入项目执行 npm init 生成 package.json文件。 接着安装上面提到的依赖包
npm install commander inquirer download-git-repo handlebars ora chalk log-symbols
接着在package.json 中加上 bin的内容:
代码语言:txt复制{
"name": "d-cli",
"version": "1.0.0",
"bin": {
"d-cli": "index.js"
},
...
}
然后在根目录创建 index.js 文件,加载所有需要用到的依赖
代码语言:JavaScript复制const fs = require("fs");
const program = require("commander");
const download = require("download-git-repo");
const handlebars = require("handlebars");
const inquirer = require("inquirer");
const ora = require("ora");
const chalk = require("chalk");
const symbols = require("log-symbols");
const exec = require("child_process").exec;
const path = require("path");
开始编写我们的第一个方法
代码语言:JavaScript复制function startCommand() {
return new Promise((resolve) => {
program
.version(
require(path.resolve(__dirname, "./package")).version,
"-v --version"
)
.command("init <name>")
.action((name) => {
if (!fs.existsSync(name)) {
inquirer
.prompt([
{
type: 'input',
name: 'name',
message: 'Please input project name'
},
{
type: 'input',
name: 'author',
message: 'Please input project author'
},
{
type: 'list',
name: 'description',
choices: ['This project is for learn cli',
'How about do a amazing job?'],
default: 0,
message: 'Please input project description'
}
])
.then((answers) => {
resolve({
name,
answers,
});
});
} else {
console.log(symbols.error, chalk.red("Project already exists!"));
}
});
program.parse(process.argv);
if (!program.args.length) {
program.help();
}
});
}
startCommand 为开启cli 命令的函数,在内部我们通过commander 包定义cli 的 version 信息(版本号与package.json 保持同步)之后就可以通过 command方法 定义我们需要的功能,<name> 字段为用户在命令关键字后输入的内容,可以在action回调中获取
代码语言:JavaScript复制program
.version(
require(path.resolve(__dirname, "./package")).version,
"-v --version"
)
.command("init <name>")
.action((name) => {})
接下来我们使用 inquirer 包的 prompt 方法来与用户进行交互,数组中的内容会按顺序来提示用户输入,对象中的 name 字段会汇总起来构成回调函数中 answers 的 key。 最后通过 Promise 的 resolve 返回我们收集到的信息。
代码语言:JavaScript复制resolve({
name,
answers,
});
此外我们还通过 fs.existsSync 判断了用户输入的 name 是否在当前目录下存在重名。 最后通过 program.parse(process.argv)
启动命令配置。
if (!program.args.length) {
program.help();
}
最后判断,当用户未输入参数时给予 help 提示帮助
下载模板
代码语言:JavaScript复制function downloadProject(name) {
return new Promise((resolve) => {
const spinner = ora("Downloading template...");
spinner.start();
download(
"https://github.com:Derrys/testing-case#cli-template",
name,
{ clone: true },
(err) => {
if (err) {
spinner.fail();
console.log(symbols.error, chalk.red(err));
} else {
spinner.succeed();
resolve();
}
}
);
});
}
downloadProject 方法接收一个 name 参数(上一步返回的 name)指定下载到的文件夹名,通过 download-git-repo
库的 download 方法下载对应仓库地址的代码。需要注意的是 download 的第一个参数 https://github.com:Derrys/testing-case#cli-template
是原本正常仓库地址 (https://github.com/Derrys/testing-case.git) 的变形写法在域名后的 url 开始位置用 :
替代 /
; 地址最后的 .git
后缀名需要删除; #cli-template
后面为仓库对应的 branch 名称。
Error: 'git clone' failed with status 128 这个报错,大多是地址配置有误
渲染模板
在被clone 的仓库中我们可以使用 handlebars 的语法定义需要被替换的值
代码语言:JSON复制// https://github.com/Derrys/testing-case.git/package.json
{
"name": "{{ name }}",
"version": "0.1.0",
"description": "{{ description }}",
"author": "{{ author }}",
"private": true,
}
然后定义所有需要被替换的文件路径集合
代码语言:Javascript复制const TEMPLATEFILES = [
// Here you can list all the file directories that need to be replaced
'package.json'
]
接着定义模板替换方法
代码语言:Javascript复制function setTemplate(fileName, meta) {
return new Promise((resolve, reject) => {
if (fs.existsSync(fileName)) {
const content = fs.readFileSync(fileName).toString();
const result = handlebars.compile(content)(meta);
fs.writeFileSync(fileName, result);
resolve();
} else {
reject();
}
});
}
最后我们就可以使用 Promise.all 等方法进行批量替换了
代码语言:javascript复制const spinner = ora("Set template files...");
spinner.start();
await Promise.all(TEMPLATEFILES.map((i) => setTemplate(path.join(__dirname, name, i), answers))).then(() => {
spinner.succeed();
}).catch(e => {
console.log(symbols.error, chalk.red(e));
spinner.fail();
})
执行Shell
在模板替换完毕之后,我们还可以进行很多 shell 脚本操作来简化项目的运行步骤。
代码语言:javascript复制function doShellJob(shell, tips) {
return new Promise((resolve, reject) => {
const spinner = ora(tips);
spinner.start();
exec(shell, (error, stdout, stderr) => {
if (error) {
spinner.fail();
console.log(symbols.error, chalk.red(err));
reject();
process.exit();
}
spinner.succeed();
resolve();
});
});
}
通过
代码语言:javascript复制await doShellJob(`cd ${name} && git init`, "Git initializing...")
await doShellJob(`cd ${name} && npm install`, "Downloading node modules...")
就可以让脚本按顺序依次执行。
最后记得当所有步骤都完成之后给用户一个友好而又显著的提示,并结束当前的 process。
代码语言:javascript复制console.log(
symbols.success,
chalk.green("Project initialization completed, enjoy you coding now!")
);
process.exit();
现在我们的脚手架工具已经搭建好了,大家可以一起来尝试下了!
全部源码
d-cli