一个CLI命令行脚手架工具 柔光的暖阳◎ 2022-11-12 04:11 181阅读 0赞 ## 前言 ## 开始本篇文章前,我们先来思考几个问题: * 平时自己创建新项目的流程是怎么样的? * 团队为了落地规范化(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 然后现在的目录结构就是: ![图片][7219defb8c9f7623a8e167d9480182fa.png] ## 脚本映射为命令 ## 初始化项目后,接下来有一步很重要的操作:把脚本映射为命令。 具体操作就是在`package.json`文件中添加: "bin": { "senlin": "./index.js" }, 有了脚本后,怎么把脚本链接到全局呢(其实就是像你执行`vue`命令一样)? 这里只用在`当前项目目录`下执行`npm link`就可以了:![图片][4580abc940078ede5779f53b8636099a.png] 执行完`npm link`,这时你在命令行输入`senlin`便可以得到如下输出:![图片][33b41dc0f84e398db4a446db2cf5ba29.png] ## 准备模版 ## 针对我们的业务场景,我准备了两套模板: 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`给我们提供了很多便捷的指令:![图片][f75ef8234d433f708c84ab93a74f747e.png] 这对于我们创建项目是很友好的,我这里也提供了几条指令: * `-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`的整体流程图: ![图片][a3eb24433d2ee66644f7f4e7505253c2.png] 下面我将按照流程图从左到右一次进行解析。 ## senlin -V ## > “ > > 注意我们最开始在`package.json`文件的`bin`里面写入的脚本为`"senlin": "./index.js"`,也就是我们全局的指令为`senlin` 这个没什么好说的,就是用来输出当前`cli`的版本号: ![图片][ca1987fbdd96b8221452e7727578f360.png] ## senlin -l ## 这个是查看可用模版列表,目前我们有两套模板。这里针对`senlin -l`的处理是直接输出所有可用的模版信息: if (program.opts() && program.opts().list) { // 查看可用模版列表 for (let key in templates) { console.log(`${key} : ${templates[key].description}`); } } 命令行输入`senlin -l`可看到: ![图片][2631a6e07558a886a372a3fcef45a749.png] ## senlin -h ## 也就是帮助信息,是根据`commander`已知的信息自动生成的: ![图片][5ce5bfb2e436ad6a46e8936eef642aa7.png] ## 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`:![图片][cc59ff10d4f1f0666fb6c9b67986536f.png] 执行完成,本地就会生成一个`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* [7219defb8c9f7623a8e167d9480182fa.png]: /images/20221022/e1f3cf521606481f86575174d127c012.png [4580abc940078ede5779f53b8636099a.png]: /images/20221022/38bfda6b99404c1296c794e6116a48d9.png [33b41dc0f84e398db4a446db2cf5ba29.png]: /images/20221022/318b787fe8f04a00b414b1d6c3b4eda7.png [f75ef8234d433f708c84ab93a74f747e.png]: /images/20221022/8e8479eb298b4e718b5b2ef6af8f96ac.png [a3eb24433d2ee66644f7f4e7505253c2.png]: /images/20221022/81396fd735f54b0c95c83008d3d6e5cd.png [ca1987fbdd96b8221452e7727578f360.png]: /images/20221022/841ee2597a754393ace8077625b875b9.png [2631a6e07558a886a372a3fcef45a749.png]: /images/20221022/e3d0bd3caf8449d48981d5d2bf9588cd.png [5ce5bfb2e436ad6a46e8936eef642aa7.png]: /images/20221022/a89c96887d854ce38f8de1cabd68f7b7.png [cc59ff10d4f1f0666fb6c9b67986536f.png]: /images/20221022/8e5156d4b0664fb6b517e0adb7d22ab4.png
相关 如何利用Java CLI进行命令行工具的编写 在Java中,你可以使用Java CLI(Command Line Interface)库来编写命令行工具。以下是一个基本步骤: 1. **引入依赖**:如果你还没有在你的项 分手后的思念是犯贱/ 2024年09月11日 01:45/ 0 赞/ 14 阅读
相关 【Go课件】golang命令行应用脚手架工具:urfave cli入门示例 urfave/cli 是一个用于创建命令行应用程序的 Go 语言库,可以让你轻松地定义和解析命令行参数,并且可以生成帮助文档和自动补全等功能。下面是一个简单的示例,演示如何使用 谁践踏了优雅/ 2024年03月25日 06:21/ 0 赞/ 31 阅读
相关 vue-cli脚手架 案例一: ![在这里插入图片描述][22bc07d894c44c35b976d5c6d9903b4b.png] ![在这里插入图片描述][6ab9f79185474454 左手的ㄟ右手/ 2024年03月23日 11:48/ 0 赞/ 49 阅读
相关 vue 实现命令行界面_Vue脚手架Vue-cli(命令行和图形化初始化项目) 一、Vue-cli安装 vue-cli是vue官方提供的脚手架工具,默认搭建好了一个项目的基本架子,我们在其基础上进行相应修改即可。 全局安装: npm install 淩亂°似流年/ 2023年01月01日 01:39/ 0 赞/ 202 阅读
相关 一个CLI命令行脚手架工具 前言 开始本篇文章前,我们先来思考几个问题: 平时自己创建新项目的流程是怎么样的? 团队为了落地规范化(git 提交规范、代码规范、文档规范等),做了哪些事 柔光的暖阳◎/ 2022年11月12日 04:11/ 0 赞/ 182 阅读
相关 Node 如何开发一个命令行工具 最近山月开发了一个从任意 URL 解析内容并生成 `markdown` 的小客户端工具: markdown-read。用以我个人公众号的内容获取及一些优质内容的整理收藏,欢迎 超、凢脫俗/ 2022年10月27日 12:13/ 0 赞/ 183 阅读
相关 『.NET Core CLI工具文档』(一).NET Core 命令行工具(CLI) > 说明:本文是个人翻译文章,由于个人水平有限,有不对的地方请大家帮忙更正。 > 原文:[.NET Core Command Line Tools][] > 翻译:[. 柔光的暖阳◎/ 2022年09月24日 05:16/ 0 赞/ 159 阅读
相关 Flask内置命令行工具—CLI 应用发现 flask命令在[Flask][]库安装后可使用,使用前需要正确配置`FLASK_APP`环境变量以告知用户程序所在位置。不同平台设置方式有所不同。 Unix 浅浅的花香味﹌/ 2022年04月03日 12:48/ 0 赞/ 736 阅读
相关 使用cobra创建cli命令行工具 什么是cobra? Cobra既是用于创建强大的现代CLI应用程序的库,也是用于生成应用程序和命令文件的程序。 Cobra是一个库,提供了一个简单的界面来创建类似 Dear 丶/ 2022年03月18日 05:52/ 0 赞/ 182 阅读
还没有评论,来说两句吧...