一、初始化cli工具
(1)创建文件
- 创建一个文件夹
npm init -y
- 修改 package.json文件,添加以下属性
{
"name": "welkin-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": { //以下是我添加的
"welkin-cli": "./bin/welkin.js"//这个地方需要把文件名写全,否则npm link报错
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^8.1.0"
}
}
- 添加之后说明需要在根目录下创建一个bin文件夹,创建welkin.js文件作为入口文件
//node环境使用
`#! /usr/bin/env node
//下面这一行需要创建main文件
require('../src/main.js');
- 创建src/main.js入口文件
console.log('我是测试文件');
(2)执行文件
- 链接包到全局下使用
npm link
这个地方需要sudo输入密码,且需要上面package.json中指定文件夹仔细
- 执行
welkin-cli
输出:
代表创建完成
二、动态获取版本号
(1)创建
- 下载自定义输出工具
npm install commander
npm地址:https://www.npmjs.com/package/commander
介绍几个常用的方法如下:
- version:设置version
- parse:解析用户传递过来的参数
- command:设置自定义命令
- option:可选参数 (选项)
- alias:匹配
- description:命令描述
- action:执行对应命令后所执行的方法。
- 获取根目录下的package.json中的版本信息
const { version } = require('../package.json');
module.exports = {
version,
};
- 入口文件中使用命令输出
//解析输入参数
const program = require('commander');
const { version } = require('./constants');
program.version(version).parse(process.argv);v
(2)使用
命令行输入
welkin-cli -V
可以看到和上面的package.json文件中的版本信息一模一样
三、自定义配置命令
(1)配置reflect
- 配置json命令
alias - 匹配规则
description - 描述
examples - 样例
const cmdActions = {
create: {
alias: 'c', //匹配到c
description: 'create a project', //描述
examples: [
'welkin-cli create <project-name>', //例子
],
},
config: {
alias: 'conf',
description: 'config project variable',
examples: ['welkin-cli config set <k><v>', 'welkin-cli config ser <k>'],
},
'*': {
alias: '',
description: 'command not found',
examples: [],
},
};
- reflect
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect
不是一个函数对象,因此它是不可构造的。
详情见MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
这里使用Reflect.ownKeys()方法,该方法返回一个由目标对象自身的属性键组成的数组
(2)配置命令行数组
Reflect.ownKeys(cmdActions).forEach((item) => {
program
.command(item)
.alias(cmdActions[item].alias)
.description(cmdActions[item].description)
.action(() => {
if (item === '*') {
//当输入未匹配的命令时,输出提示
console.log(cmdActions[item].description);
} else {
//输入匹配到命令后,xxx
console.log(item);
}
});
});
(3)配置help命令
配置help命令就是用户在终端中输入命令之后,需要显示所有命令
program.on('--help', () => {
console.log('\nExample:');
Reflect.ownKeys(cmdActions).forEach((item) => {
cmdActions[item].examples.forEach((i) => {
console.log(`${i}`);
});
});
});
注意:这个地方一定要将program.parse(process.argv);
放在整个文件的最下面,不然输入的命令可能监听不全
四、配置create命令
(1)配置路径
一般情况下,输入create命令后,需要进行一系列的操作,所以我们将上面的代码进行修改
Reflect.ownKeys(cmdActions).forEach((item) => {
program
.command(item)
.alias(cmdActions[item].alias)
.description(cmdActions[item].description)
.action(() => {
if (item === '*') {
//当输入未匹配的命令时,输出提示
console.log(cmdActions[item].description);
} else {
//匹配到对应命令后进入对应的文件下
require(path.resolve(__dirname, item))(...process.argv.slice(3));
}
});
});
所以当我们没有创建create文件时,会报以下错误:
(2)创建create文件
const create = (projectName) => {
console.log(projectName);
};
module.exports = create;
先测试文件是否找到,是否输出对应代码。
执行welkin-cli create project 可以打印出projectName
(3)通过axios拉取github上的模板信息
1. 安装axios
npm install axios
2. 获取github上的模板信息
github地址:https://github.com/td-cli
可以看到里面有两个模板
现在来获取一下这个仓库的信息,或者这个组织仓库的信息时,是需要通过api.github
去获取的【这个是github提供的一个json文件,可以自己设置,设置方式如下:
GitHub api设置方法:https://blog.csdn.net/qq_15174755/article/details/83306497
我们现在使用的模板json如下:
https://api.github.com/orgs/td-cli/repos
代码如下:
//引入axios
const axios = require('axios');
//获取github 仓库信息
const handleFetchRepoList = async () => {
//解构赋值,获取该接口下的data对象,并返回
const { data } = await axios.get('https://api.github.com/orgs/td-cli/repos');
return data;
};
const create = async (projectName) => {
// console.log(projectName);
let repos = await handleFetchRepoList();
//将json数组中的name赋给repos【重写repos】
repos = repos.map((item) => item.name);
console.log(repos);
};
module.exports = create;
输入welkin-cli create xxx
就可以获取模板名称
3. 设置loading加载样式
a. 安装
npm install ora
b. 函数如下
const ora = require('ora');
//loading function
const handleLoading = (fn, msg) => async (...args) => {
const spinner = ora(msg);
spinner.start(); //loading start
const result = await fn(...args); //fn 函数调用
spinner.succeed(); //loading end
return result;
};
c. 调用如下
const create = async (projectName) => {
let repos = await handleLoading(
handleFetchRepoList,
'fetching template...'
)();
//将json数组中的name赋给repos【重写repos】
repos = repos.map((item) => item.name);
console.log(repos);
};
module.exports = create;
注意:
这个地方如果报错,只能说明版本不一样
贴一下我的package.json
"dependencies": {
"axios": "^0.19.0",
"commander": "^4.0.1",
"consolidate": "^0.15.1",
"download-git-repo": "^3.0.2",
"ejs": "^3.0.1",
"ini": "^1.3.5",
"inquirer": "^7.0.0",
"metalsmith": "^2.3.0",
"ncp": "^2.0.0",
"ora": "^4.0.3",
"util": "^0.12.1"
}
这样当你输入welkin-cli create xxx
之后就能看到loading的作用了
4. 设置终端选择功能
a. 安装
npm install inquirer
c. 函数如下
const create = async (projectName) => {
let repos = await handleLoading(
handleFetchRepoList,
'fetching template...'
)();
//将json数组中的name赋给repos【重写repos】
repos = repos.map((item) => item.name);
//配置选择功能
const { repo } = await Inquirer.prompt({
name: 'repo',//用json中已存在的键名
type: 'list',
message: 'please choice a templete to create project:',
choices: repos,
});
//repo 拿到最后选择的模板名称
console.log(repo);
};
module.exports = create;
这样当你输入welkin-cli create xxx
之后,选择对应的模板就可以完成选择。
5. 获取github模板上的版本信息
vue-template这个模板对应有多个版本号,可以自己点开看看
a. 获取version函数
//获取templete version 版本信息
const handleFetchRepoVersion = async (repo) => {
const { data } = await axios.get(
`https://api.github.com/repos/td-cli/${repo}/tags`
);
return data;
};
b. 配置获取version
该函数在create函数中获取到repo下面添加即可:【后续代码不再贴完整】
let tags = await handleLoading(
handleFetchRepoVersion,
'fetching version...'
)(repo);
tags = tags.map((item) => item.name);
const { tag } = await Inquirer.prompt({
name: 'tag',
type: 'list',
message: 'please choice a tag to create project:',
choices: tags,
});
console.log(tag);
五、下载模板
(1)暂时存放模板
Constants.js 文件中配置
const { version } = require('../package.json');
const downloadDirectory = `${
process.env[process.platform === 'darwin' ? 'HOME' : 'USERPROFILE']
}/.template`;
module.exports = {
version,
downloadDirectory,
};
(2)配置下载函数
1. 安装
npm i download-git-reop
因为这个方法不是promise方法,所以需要自己包装成promise。
需要安装promise
npm i util
包装方式如下:
const { promisify } = require('util');
let downloaaGitRepo = require('download-git-repo');//注意是let用法
downloaaGitRepo = promisify(downloaaGitRepo);
2. 函数如下
// 引入路径
const { downloadDirectory } = require('./constants');
// download funtion
const handleDownload = async (repo, tag) => {
let api = `td-cli/${repo}`; //配置api
if (tag) {
//有的模板没有版本号
api += `#${tag}`;
}
const dest = `${downloadDirectory}/${repo}`; //获取目录
//下载start
await downloaaGitRepo(api, dest);
return dest;//返回当前模板存放的路径
};
可以实现模板的下载
(3)将模板拷贝到指定目录下
因为我们上一步是自定义的目录,但我们在创建项目的时候,是需要在该目录下下载模板的,所以这时候就需要将模板进行拷贝到当前指定目录下。
注意:【以下两点自己实现】
- 这个地方需要判断创建的目录是否已经存在,存在则提示并创建失败
- 模板下载自定义目录有个好处就是可以缓存下来,当下一次创建项目时,如果模板一样就可以不用下载,可以走缓存
1. 安装ncp进行文件拷贝
npm i ncp
并对ncp进行promise包装
let ncp = require('ncp');
ncp = promisify(ncp);
2. 判断模板中是否有ask.js文件
在拷贝文件的时候需要判断一个文件ask.js
【复杂模板】
说明该模板需要编译才能下载
需要用到fs模板进行读文件,metalsmith遍历文件夹
安装加引入:
npm i metalsmith
const MetalSmith = require('metalsmith');
3. 有ask文件后需要按照用户输入进行编译
这里需要用到render模块
下载:
npm i render
引入加包装:
let { render } = require('ejs');
render = promisify(render);
(4)拷贝函数及下载函数如下
// 3. download
const result = await handleLoading(handleDownload, 'downloading templete...')(
repo,
tag
);
// 4. 判断该模板是否有ask文件
if (!fs.existsSync(path.join(result, 'ask.js'))) {
await ncp(result, path.resolve(projectName));
} else {
//有ask文件,需要让用户填写信息,事后进行编译
await new Promise((resolve, reject) => {
MetalSmith(__dirname) //如果传入路径,会默认遍历该路径下的src文件夹
.source(result) //全局搜索
.destination(path.resolve(projectName)) //设置存储位置
.use(async (files, metal, done) => {
const args = require(path.join(result, 'ask.js'));
const obj = await Inquirer.prompt(args);
const meta = metal.metadata();
Object.assign(meta, obj);
delete files['ask.js'];
done();
})
.use(async (files, metal, done) => {
//遍历信息,让用户填写
const obj = metal.metadata();
Reflect.ownKeys(files).forEach(async (item) => {
if (item.includes('js') || item.includes('json')) {
let content = files[item].contents.toString(); //获取文件内容
if (content.includes('<%')) {
content = await render(content, obj);
files[item].contents = Buffer.from(content);
}
}
});
done();
})
.build((err) => {
if (err) {
reject();
} else {
resolve();
}
});
});
}
当你选择有ask的文件之后就会出现以下显示:
并在你创建的这个项目下创建一个自己配置的文件,类似下面:
参考文章:https://github.com/Tie-Dan/tdsp-cli
参考视频:https://www.bilibili.com/video/BV1w54y1B7Tb?p=6