前言
开始本篇文章前,我们先来思考几个问题:
平时自己创建新项目的流程是怎么样的?
团队为了落地规范化(git 提交规范、代码规范、文档规范等),做了哪些事情?
我想大部分同学肯定都是这样回答的:现在社区都有开箱即用的脚手架,像vue-cli
、create-react-app
这种,我们直接用脚手架来创建项目就可以了啊。
上面这种方式也是我所在的团队最开始的基操
,但是随着团队成员的快速增加和业务的飞速迭代,有很多问题逐渐暴露出来:
大部分业务场景是相似的,那么对于基础框架结构的诉求(这里包括工具类、接口封装、环境变量配置、eslint 配置、git-hook 等)都是一样的。如果每次大家都从零开始,那么只会徒增很多毫无意义的重复性工作。
这里你可能会说:那我们简单的复制粘贴就可以了啊~
那你有没有感觉这种方式不太优雅呢?暂且不去评估这种方式的优缺点,如果后续基础框架结构发生调整,那么你是不是要继续坚持cv
大法呢?
上面说了这么多,其实就是两个重点:
效率
复用性
我们团队内部也是发现了上述问题,结合自己的具体业务场景,自研了一套cli
,主要也是基于Vue Cli
打造而来,功能包含:
支持基于
Vue
和React
的不同模板统一的项目目录结构
丰富的工具类库
初始化配置文件
预定义的共用组件
丰富的命令行提示
这里关于Vue-Cli
的具体操作我就不演示了,直接进入正题。
需要做哪些准备
其实,也就是来看下主要借助了哪些第三方库的能力:
commander.js[1],可以自动的解析命令和参数,用于处理用户输入的命令。
download-git-repo[2],下载并提取 git 仓库,用于下载项目模板。
Inquirer.js[3],通用的命令行用户界面集合,用于和用户进行交互。
ora[4],下载过程久的话,可以用于显示下载中的动画效果。
chalk[5],可以给终端的字体加上颜色。
log-symbols[6],可以在终端上显示出 √ 或 × 等的图标。
这些第三方库的链接我都有在文中标出,对应的api
也都相对简单,大家可以自行前往查看具体的使用,这里就不展开说明了。
初始化项目
首先创建一个空项目,命名为 cosen-cli
,然后新建一个 index.js
文件,并写入:
-
#!/usr/bin/env node
-
// 使用Node开发命令行工具所执行的JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明
-
-
console.log(
'senlin-cli初始化...')
再执行 npm init
生成一个 package.json
文件。最后安装上面需要用到的依赖。
npm install commander download-git-repo inquirer ora chalk log-symbols
然后现在的目录结构就是:
脚本映射为命令
初始化项目后,接下来有一步很重要的操作:把脚本映射为命令。
具体操作就是在package.json
文件中添加:
-
"bin": {
-
"senlin":
"./index.js"
-
},
有了脚本后,怎么把脚本链接到全局呢(其实就是像你执行vue
命令一样)?
这里只用在当前项目目录
下执行npm link
就可以了:
执行完npm link
,这时你在命令行输入senlin
便可以得到如下输出:
准备模版
针对我们的业务场景,我准备了两套模板:
-
const templates = {
-
"ts-vue": {
-
url:
"https://github.com/easy-wheel/ts-vue",
-
downloadUrl:
"https://github.com:easy-wheel/ts-vue#master",
-
description:
-
"ts-vue是一个中后台前端解决方案,它基于 vue, typescript 和 element-ui实现。",
-
},
-
"umi-hooks": {
-
url:
"https://github.com/easy-wheel/Umi-hooks",
-
downloadUrl:
"https://github.com:easy-wheel/Umi-hooks#master",
-
description:
-
"Umi-Hooks是一个中后台前端解决方案,它基于 umi, react, typescript 和 ant-design实现。",
-
},
-
};
“这里关于
downloadUrl
有一点需要说明的是,url
的格式须为:<host>:<userName>/<repo> <projectName> #branchName
,否则会报'git clone' failed with status 128
,具体可参考issue[7]
这里顺便贴下模板地址:
ts-vue[8]
umi-hooks[9]
也欢迎大家多多 star 啊!
commander 解析命令行参数
我们知道vue-cli
给我们提供了很多便捷的指令:
这对于我们创建项目是很友好的,我这里也提供了几条指令:
-i
:初始化项目-V
:查看版本号信息-l
:查看可用模版列表-h
:查看帮助信息
对应代码:
-
const program = require(
"commander");
-
-
program
-
.version(packageData.version)
-
.option(
"-i, --init",
"初始化项目")
-
.option(
"-V, --version",
"查看版本号信息")
-
.option(
"-l, --list",
"查看可用模版列表")
-
program.parse(process.argv);
这里,我针对上面用到的相关api
依次做下说明:
version
作用
用于定义命令程序的版本号
option
作用
定义命令的选项
参数说明
它接受四个参数,在第一个参数中,它可输入短名字 -i
和长名字–-init
,使用 |
或者,
分隔,在命令行里使用时,这两个是等价的,区别是后者可以在程序里通过回调获取到;第二个为描述, 会在 help
信息里展示出来;第三个参数为回调函数,他接收的参数为一个string
,有时候我们需要一个命令行创建多个模块,就需要一个回调来处理;第四个参数为默认值
parse
作用
用于解析process.argv
ok,到这里我们的前序工作就基本完成了。我这里梳理了一张cosen-cli
的整体流程图:
下面我将按照流程图从左到右一次进行解析。
senlin -V
“注意我们最开始在
package.json
文件的bin
里面写入的脚本为"senlin": "./index.js"
,也就是我们全局的指令为senlin
这个没什么好说的,就是用来输出当前cli
的版本号:
senlin -l
这个是查看可用模版列表,目前我们有两套模板。这里针对senlin -l
的处理是直接输出所有可用的模版信息:
-
if (program.opts() && program.opts().list) {
-
// 查看可用模版列表
-
for (let key in templates) {
-
console.log(
`${key} : ${templates[key].description}`);
-
}
-
}
命令行输入senlin -l
可看到:
senlin -h
也就是帮助信息,是根据commander
已知的信息自动生成的:
senlin -i
这条指令是用来初始化模板的,也是目前cosen-cli
中比较重要且复杂的一条了。
我们结合上文的流程图来梳理下这块的逻辑:
首先,利用inquirer
提供给用户输入自定义信息(包含项目名称、项目简介、作者名称、选择项目模版)。对应代码就是:
-
inquirer
-
.prompt([
-
{
-
type:
"input",
-
name:
"projectName",
-
message:
"请输入项目名称",
-
},
-
{
-
type:
"input",
-
name:
"description",
-
message:
"请输入项目简介",
-
},
-
{
-
type:
"input",
-
name:
"author",
-
message:
"请输入作者名称",
-
},
-
{
-
type:
"list",
-
name:
"template",
-
message:
"选择其中一个作为项目模版",
-
choices: [
"ts-vue (vue+ts项目模版)",
"umi-hooks (react+ts项目模版)"],
-
},
-
])
-
.then((answers) => {
-
// 把采集到的用户输入的数据解析替换到 package.json 文件中
-
console.log(
"选择", answers.template.split(
" ")[
0]);
通过answers
可以获取到用户输入的信息,接下来我们要做的就是检查用户输入的项目名称是否已存在,防止已有项目被覆盖。这里对应是checkName
。
checkName
-
// 创建项目前校验是否已存在
-
function checkName(projectName) {
-
return
new Promise((resolve, reject) => {
-
fs.readdir(process.cwd(), (err, data) => {
-
if (err) {
-
return reject(err);
-
}
-
if (data.includes(projectName)) {
-
return reject(
new Error(
`${projectName} already exists!`));
-
}
-
resolve();
-
});
-
});
-
}
校验完项目名称,接下来就是下载对应代码模板了,对应downloadTemplate
。
downloadTemplate
-
function downloadTemplate(gitUrl, projectName) {
-
const spinner = ora(
"download template......").start();
-
-
return
new Promise((resolve, reject) => {
-
download(
-
gitUrl,
-
path.resolve(process.cwd(), projectName),
-
{ clone:
true },
-
function (err) {
-
if (err) {
-
return reject(err);
-
spinner.fail();
// 下载失败提示
-
}
-
spinner.succeed();
// 下载成功提示
-
resolve();
-
}
-
);
-
});
-
}
可以看到在下载代码的过程中,我们使用了spinner
来营造loading
的效果,这也是为了避免拉取代码时间过久,用户得不到及时的反馈。
无论代码拉取成功或者失败,最终都会通过spinner.succeed()
或者spinner.fail()
来结束spinner
。
到这里,模板也拉取了。但还有一步没有做:用户通过交互式的命令行输入的项目名、作者、项目简介等信息我们并没有写入到本地的模板代码中。
下面,我们来完成这部分的工作,对应changeTemplate
。
changeTemplate
-
async function changeTemplate(customContent) {
-
// name description author
-
const { projectName =
"", description =
"", author =
"" } = customContent;
-
return
new Promise((resolve, reject) => {
-
fs.readFile(
-
path.resolve(process.cwd(), projectName,
"package.json"),
-
"utf8",
-
(err, data) => {
-
if (err) {
-
return reject(err);
-
}
-
let packageContent = JSON.parse(data);
-
packageContent.name = projectName;
-
packageContent.author = author;
-
packageContent.description = description;
-
fs.writeFile(
-
path.resolve(process.cwd(), projectName,
"package.json"),
-
JSON.stringify(packageContent, null,
2),
-
"utf8",
-
(err, data) => {
-
if (err) {
-
return reject(err);
-
}
-
resolve();
-
}
-
);
-
}
-
);
-
});
-
}
ok,到这里,我们整个cosen-cli
的功能就介绍和解析完成了。
下面让我们来看下最终的效果。我们在命令行执行senlin -i
:
执行完成,本地就会生成一个senlin-cli-template
的文件夹,对应就是我们采用umi-hooks
生成的模板。这时我们打开文件夹的package.json
文件:
-
{
-
"name":
"senlin-cli-template",
-
"author":
"fengshuan",
-
"description":
"cli模板"
-
"private":
true,
-
"scripts": {
-
"start":
"umi dev",
-
"build":
"umi build",
-
"test":
"umi test",
-
// ...
-
},
-
"dependencies": {
-
// ...
-
},
-
"devDependencies": {
-
// ...
-
},
-
}
可以发现对应字段已经是用户自定义的字段了。
完整代码
最后贴下完整的代码,今天介绍的这些只是cosen-cli
中的比较基础的一部分,我们针对业务在cli
上做了很多事情。本文只是简单的向大家介绍一下如何基于业务开发自己的脚手架。
下面是完整代码:
-
#!/usr/bin/env node
-
// 使用Node开发命令行工具所执行的JavaScript脚本必须在顶部加入 #!/usr/bin/env node 声明
-
-
const program = require(
"commander");
-
const download = require(
"download-git-repo");
-
const inquirer = require(
"inquirer");
-
const ora = require(
"ora");
-
const chalk = require(
"chalk");
-
const packageData = require(
"./package.json");
-
const handlebars = require(
"handlebars");
-
const logSymbols = require(
"log-symbols");
-
const fs = require(
"fs");
-
const path = require(
"path");
-
-
const templates = {
-
"ts-vue": {
-
url:
"https://github.com/easy-wheel/ts-vue",
-
downloadUrl:
"https://github.com:easy-wheel/ts-vue#master",
-
description:
-
"ts-vue是一个中后台前端解决方案,它基于 vue, typescript 和 element-ui实现。",
-
},
-
"umi-hooks": {
-
url:
"https://github.com/easy-wheel/Umi-hooks",
-
downloadUrl:
"https://github.com:easy-wheel/Umi-hooks#master",
-
description:
-
"Umi-Hooks是一个中后台前端解决方案,它基于 umi, react, typescript 和 ant-design实现。",
-
},
-
};
-
-
program
-
.version(packageData.version)
-
.option(
"-i, --init",
"初始化项目")
-
.option(
"-V, --version",
"查看版本号信息")
-
.option(
"-l, --list",
"查看可用模版列表");
-
program.parse(process.argv);
-
if (program.opts() && program.opts().init) {
-
// 初始化项目
-
inquirer
-
.prompt([
-
{
-
type:
"input",
-
name:
"projectName",
-
message:
"请输入项目名称",
-
},
-
{
-
type:
"input",
-
name:
"description",
-
message:
"请输入项目简介",
-
},
-
{
-
type:
"input",
-
name:
"author",
-
message:
"请输入作者名称",
-
},
-
{
-
type:
"list",
-
name:
"template",
-
message:
"选择其中一个作为项目模版",
-
choices: [
"ts-vue (vue+ts项目模版)",
"umi-hooks (react+ts项目模版)"],
-
},
-
])
-
.then((answers) => {
-
// 把采集到的用户输入的数据解析替换到 package.json 文件中
-
console.log(
"选择", answers.template.split(
" ")[
0]);
-
let url = templates[answers.template.split(
" ")[
0]].downloadUrl;
-
initTemplateDefault(answers, url);
-
});
-
}
-
if (program.opts() && program.opts().list) {
-
// 查看可用模版列表
-
for (let key in templates) {
-
console.log(
`${key} : ${templates[key].description}`);
-
}
-
}
-
-
async function initTemplateDefault(customContent, gitUrl) {
-
console.log(
-
chalk.bold.cyan(
"CosenCli: ") +
"will creating a new project starter"
-
);
-
const { projectName =
"" } = customContent;
-
-
try {
-
await checkName(projectName);
-
await downloadTemplate(gitUrl, projectName);
-
await changeTemplate(customContent);
-
-
console.log(chalk.green(
"template download completed"));
-
console.log(
-
chalk.bold.cyan(
"CosenCli: ") +
"a new project starter is created"
-
);
-
} catch (error) {
-
console.log(chalk.red(error));
-
}
-
}
-
-
// 创建项目前校验是否已存在
-
function checkName(projectName) {
-
return
new Promise((resolve, reject) => {
-
fs.readdir(process.cwd(), (err, data) => {
-
if (err) {
-
return reject(err);
-
}
-
if (data.includes(projectName)) {
-
return reject(
new Error(
`${projectName} already exists!`));
-
}
-
resolve();
-
});
-
});
-
}
-
-
function downloadTemplate(gitUrl, projectName) {
-
const spinner = ora(
"download template......").start();
-
-
return
new Promise((resolve, reject) => {
-
download(
-
gitUrl,
-
path.resolve(process.cwd(), projectName),
-
{ clone:
true },
-
function (err) {
-
if (err) {
-
return reject(err);
-
spinner.fail();
// 下载失败提示
-
}
-
spinner.succeed();
// 下载成功提示
-
resolve();
-
}
-
);
-
});
-
}
-
-
async function changeTemplate(customContent) {
-
// name description author
-
const { projectName =
"", description =
"", author =
"" } = customContent;
-
return
new Promise((resolve, reject) => {
-
fs.readFile(
-
path.resolve(process.cwd(), projectName,
"package.json"),
-
"utf8",
-
(err, data) => {
-
if (err) {
-
return reject(err);
-
}
-
let packageContent = JSON.parse(data);
-
packageContent.name = projectName;
-
packageContent.author = author;
-
packageContent.description = description;
-
fs.writeFile(
-
path.resolve(process.cwd(), projectName,
"package.json"),
-
JSON.stringify(packageContent, null,
2),
-
"utf8",
-
(err, data) => {
-
if (err) {
-
return reject(err);
-
}
-
resolve();
-
}
-
);
-
}
-
);
-
});
-
}
-
参考资料
[1]
commander.js: https://github.com/tj/commander.js
[2]download-git-repo: https://www.npmjs.com/package/download-git-repo
[3]Inquirer.js: https://github.com/SBoudrias/Inquirer.js
[4]ora: https://github.com/sindresorhus/ora
[5]chalk: https://github.com/chalk/chalk
[6]log-symbols: https://github.com/sindresorhus/log-symbols
[7]issue: https://github.com/wuqiong7/Note/issues/17
[8]ts-vue: https://github.com/easy-wheel/ts-vue
[9]umi-hooks: https://github.com/easy-wheel/Umi-hooks
转载:https://blog.csdn.net/MrWeb/article/details/112975871