前言: 从事nodeJS开发多年,回想自己的node学习之路,经历了好几个node框架,一开始看文档也是模模糊糊,好多理念不理解。在研读计算机网络、计算机操作系统和浏览器书籍,知识打牢之后又读了node经典入门数据《深入浅出NodeJS》,朴灵写的,读了三遍,对node的基础思想和知识架构在脑海里慢慢成型;

从异步I/O事件到libuv的线程池到node事件处理机制,从node模块机制,到内存控制垃圾回收,再到V8引擎,再到stream(网络流,文档流)… nodejs虽然应用层上是Javascript,依赖V8引擎解析调用libuv抹平系统之间的差异,底层的内建模块都是C#语言编写,所以内置模块运行速度相比java并不逊色;而且依赖于libuv线程池+nodeJS事件运行机制,node在处理低cpu密集高异步I/O的情境下游刃有余。
但是nodeJS的缺点也很明显:

  • 不适合CPU密集型应用
  • 基于单进程导致多核CPU的利用率低,即使加了cluster,对qps的提高也不是线性的。
  • 可靠性低

但是这并不妨碍前端的同学通过NodeJS打开后端的大门,接触更多的后端知识。


现在nodeJS的使用场景有开发工具/脚手架BFFSSR反向代理等;

接下来带大家入门一个实用的git 自动创建远程repo并提交代码的工具,通过这个工具可以带你入门nodeJS,你可以学到fs模块、process模块、path模块、console模块等基础模块, 那么如何用nodeJS创建一个CLI APP(命令行工具)呢?

创建一个远程仓库需要几步?

  1. 首先登陆gitlab
  2. 创建一个新的project
  3. 本地,git init初始化本地仓库
  4. 添加.gitignore文件
  5. git add
  6. git commit -m “Initial commit”
  7. git remote add origin
  8. git push -u origin master

我们创建一个node命令行工具去简化这些步骤;

需要的知识:

  1. 基础的node知识
  2. node环境,建议node版本高于V13.2.0

Node verison 13.2.0 起开始正式支持 ES Modules 特性。

  1. git 程序

1. 首先我们Npm init初始化一个项目,新建一个index.js的文件存当做我们app的入口文件;文件中引入我们cli常用的npm包,如下:

  1. #! /usr/bin/env node
  2. // 提供可执行的terminal 命令
  3. const app = require("commander");
  4. // 允许给terminal的文字加颜色
  5. const chalk = require("chalk");
  6. // terminal 清屏
  7. const clear = require("clear");
  8. // 提供大字体的文字
  9. const figlet = require("figlet");
  10. // 交互式的命令行工具,询问问题收集回答
  11. const inquirer = require("inquirer");
  12. // 使用 personal access token登录github

还有其他开发cli工具好用的npm包:

  • ora 优雅的终端加载器 image.png
  • shelljs 封账了child_process的模块,调用系统命令更加方便
  • yargs // 处理命令行参数

2. 创建一个简单的终端命令

我们想要用户在执行node index.js init命令的时候,能执行我们定义的Init方法;我们可以用commander或yargs去创建一个init命令,解析输入的命令行,然后执行自定义的操作console.log("hello world")

  1. #! /usr/bin/env node
  2. // 提供可执行的terminal 命令
  3. const app = require("commander");
  4. // 允许给terminal的文字加颜色
  5. const chalk = require("chalk");
  6. // terminal 清屏
  7. const clear = require("clear");
  8. // 提供大字体的文字
  9. const figlet = require("figlet");
  10. // 交互式的命令行工具,询问问题收集回答
  11. const inquirer = require("inquirer");
  12. // 使用 personal access token登录github
  13. //display app title
  14. console.log(
  15. chalk.redBright(
  16. figlet.textSync("SELF MADE CLI TOOL!", { horizontalLayout: "full" })
  17. )
  18. );
  19. //show welcome message
  20. console.log(chalk.yellow("Welcome to the GitHub initializer tool."));
  21. app
  22. .command("init")
  23. .description("Run Cli Tool!")
  24. .action(async () => {
  25. //show welcome message
  26. console.log("hello world");
  27. });
  28. app.parse(process.argv);
  29. //show help if no arg is passed
  30. if (!app.args.length) {
  31. app.help();
  32. }

使用chalk给文字添加红色,figlet生成大字号的文字,看下效果
commendor.gif
当我们创建好命令之后,就可以向用户发起询问是否自动创建一个远程仓库,使用inquirer库可以支持人机交互式的命令行询问,inquirer.prompt接收一个配置参数questions ,并返回一个promise对象:

inquirer.prompt(questions, answers) -> promise

  1. const question = [
  2. {
  3. name: "proceed",
  4. type: "list",
  5. message:
  6. "Proceed to push this project to a Github remote repo?(Yes/No)",
  7. choices: ["Yes", "No"],
  8. default: "0",
  9. },
  10. ];
  11. const answer = await inquirer.prompt(question);
  12. if (answer.proceed == "Yes") {
  13. console.log(chalk.blue("Yes,let' do it"));
  14. } else {
  15. //show exit message
  16. console.log(chalk.gray("Ok, bye."));
  17. }
  18. });

现在我们实现的功能有个:

  • 接受init命令,展示欢迎消息
  • 询问用户是否创建远程仓库,yes->进入下一步,no->退出

    3. git账户登录,并存储用户的登录凭证

    git远程登录通过创建git lab后台创建 personal access token,用于通过API调用gitlab的接口和登录授权;
    登录询问如下:

    1. const question = [
    2. {
    3. name: "token",
    4. type: "input",
    5. message: "Enter your github personal access token",
    6. validate: function (params) {
    7. if (params.length == 40) {
    8. return true;
    9. } else return "Please enter a vaild token";
    10. },
    11. },
    12. ];
    13. token = (await inquirer.prompt(question)).token;

    进一步优化,我们可以通过记录personal access token并存储在本地,这样每次登录就不用都输入一次;
    使用configstore 库,它会接收name生成一个json文件,存储在~/.config/configstore/**.json下面;
    企业微信20210817-190107@2x.png ```javascript const inquirer = require(“inquirer”); const packageJson = require(“./package.json”); const chalk = require(“chalk”); const got = require(“got”);

async function authenticate() { const { default: Configstore } = await import(“configstore”); const config = new Configstore(packageJson.name); //1. 尝试获取token let token = config.get(“private_token”); if (!token) { const question = [ { name: “token”, type: “input”, message: “Enter your gitlab personal access token”, validate: function(params) { if (params.length == 20) { return true; } else return “Please enter a vaild token”; }, }, ]; token = (await inquirer.prompt(question)).token; } else { console.log(“Token is found in config.”); }

try { console.log(chalk.green(“Authenticating…”)); const result = await got(“https://****/api/v4/projects“, { searchParams: { private_token: token, }, responseType: “json”, }); // 2. 存储token,下次直接取出使用 config.set(“private_token”, token); console.log(chalk.green(“authenticated!!!”)); } catch (error) { console.log(chalk.redBright(“Unauthorized, please check your token”)); config.delete(“private_token”); return false; } return true; }

  1. <a name="Tszxf"></a>
  2. ### 4. 使用gitlab API创建project
  3. 查阅git lab中文文档,找到创建project的API,支持许多参数,必填的有name和path(新项目的存储库名称. 如果未提供,则根据名称生成(生成的小写字母加短划线)),des和其他等选填;visiability支持三种选项[**"public", "private", "internal"**]; inquirer询问用户输入项目名称,描述,选则可见性;然后通过`got`库请求git lab服务端接口:[https://****/api/v4/projects](https://git.ddxq.mobi/api/v4/projects) ,接口需要携带token,创建成功会返回当前project的参数;
  4. ```javascript
  5. async function newReop() {
  6. const question = [
  7. {
  8. name: "name",
  9. type: "input",
  10. message: "Enter new repo Name",
  11. default: path.basename(process.cwd()),
  12. validate: (value) => {
  13. if (value.length) {
  14. return true;
  15. } else {
  16. return "Please enter a valid input.";
  17. }
  18. },
  19. },
  20. {
  21. name: "description",
  22. type: "input",
  23. message: "Enter new repo description (optional).",
  24. default: null,
  25. },
  26. {
  27. name: "visibility",
  28. type: "list",
  29. message: "Set repo to public or private?",
  30. choices: ["public", "private", "internal"],
  31. default: "private",
  32. },
  33. ];
  34. const answers = await inquirer.prompt(question);
  35. // 创建一个repo
  36. const data = {
  37. name: answers.name,
  38. description: answers.description,
  39. visibility: answers.visibility,
  40. };
  41. const { default: Configstore } = await import("configstore");
  42. const config = new Configstore(packageJson.name);
  43. const token = config.get("private_token");
  44. if (!token) {
  45. console.log(chalk.red("please rewrite token"));
  46. return false;
  47. }
  48. try {
  49. const response = await got.post("https://****/api/v4/projects", {
  50. json: data,
  51. searchParams: {
  52. private_token: token,
  53. },
  54. responseType: "json",
  55. });
  56. return response.body;
  57. } catch (e) {
  58. if (e.response.statusCode === 401) {
  59. console.log(chalk.red(e.response.body.message || e.response.body.error));
  60. }
  61. process.exit(1);
  62. }
  63. }

5. 配置.gitignore文件

我们的工具现在实现了登录->存储token,我们可能还需要配置.gitignore文件来确定哪些文件需要提交到远程仓库;我们可以使用glob库通过全局模式匹配的方式查找文件,glob.sync同步返回查找到的结果;

  1. async function ignoreFiles(params) {
  2. const filesToIgnore = [];
  3. const files = glob.sync("**/*", {
  4. ignore: "**/node_modules/**",
  5. });
  6. const node_modules = glob.sync("{*/node_modules/,node_modules/}");
  7. if (node_modules.length) {
  8. filesToIgnore.push(...node_modules);
  9. } else {
  10. fs.closeSync(fs.openSync(".gitignore", w));
  11. }
  12. // 创建一个问题,询问哪些文件需要忽略
  13. const question = [
  14. {
  15. name: "ignore",
  16. type: "checkbox",
  17. message: "Select the file and/or folders you wish to ignore:",
  18. choices: files,
  19. },
  20. ];
  21. const answers = await inquirer.prompt(question);
  22. // 如果用户选则了某个文件或文件夹,写入.gitignore
  23. if (answers.ignore.length) {
  24. filesToIgnore.push(...answers.ignore);
  25. fs.writeFileSync(".gitignore", filesToIgnore.join("\n"));
  26. }
  27. }

现在的展示效果如下:
未命名.gif

6. git 提交文件到远程

然后询问用户使用http还是ssh的方式设置remote;

  1. const clone_method = await inquirer.prompt({
  2. name: "clone methods",
  3. type: "list",
  4. message: "which methonds do you want do clone?",
  5. choices: ["http", "ssh"],
  6. default: "http",
  7. });
  8. const url =
  9. clone_method === "http" ? urls.http_url_to_repo : urls.ssh_url_to_repo;

使用simple-git 库可以用js调用git命令,git init和addRemote的方法支持回调函数,在回调函数中配合ora库展示loading效果,展示成功、失败信息;

  1. async function initialCommit(url) {
  2. try {
  3. function onInit(err, initResult) {
  4. if (err) {
  5. spinner.fail();
  6. console.log(chalk.red("git init error!"));
  7. return;
  8. }
  9. spinner.succeed();
  10. spinner.color = "yellow";
  11. spinner.text = `git add remote`;
  12. spinner.start();
  13. }
  14. function onRemoteAdd(err, addRemoteResult) {
  15. if (err) {
  16. spinner.fail();
  17. console.log(chalk.red("git add remote error!"));
  18. return;
  19. }
  20. spinner.succeed();
  21. }
  22. var spinner = ora("git init....").start();
  23. await git
  24. .init(onInit)
  25. .add(".gitignore")
  26. .add("./*")
  27. .commit("initial commit")
  28. .addRemote("origin", url, onRemoteAdd)
  29. .push(url, "master", ["-u"]);
  30. return true;
  31. } catch (e) {
  32. console.log(e);
  33. process.exit(1);
  34. }
  35. }

现在的效果是:

  1. 输入命令node index.js init (或者使用script 脚本启动文件)
  2. 欢迎信息,询问是否创建一个远程仓库 yes->步骤3,no->退出
  3. 登录,输入personal access token,如果之前输入过则从configstore文件中读取
  4. 输入项目名,描述,可见等参数,创建project
  5. 选则需要git 忽略文件或文件夹,写入.gitignore
  6. 使用git命令创建remote,提交代码
  7. 提示成功消息,done

final.gif
总结一下:
我们通过练习这个工具的编写,可以掌握如何编写一个cli App,顺便也学会了如何编写,调试,打包上线一个npm库;里面有一些基础的nodejs知识,比如

  • node模块
  • commonJS规范
  • node中如何使用esmodule
  • 各种做cli可能会用到的库,比如commonder,chalk,figlt,got,inquirer,simple-git,configstore,glob,ora
  • 常用的node基础模块

需要看源码私信我
参考资料:

  1. git lab中文文档[https://www.bookstack.cn/read/gitlab-doc-zh/docs-353.md#bqaa2e]
  2. 各种npm库…省略
  3. nodeJS文档 [http://nodejs.cn/api/]