从零开始制作一个cli工具
发布于 5 年前 作者 fruit-memory 3834 次浏览 来自 分享

进入正题

1、首先新建一个文件夹dva-native-cli,然后执行npm init,当然你也可以自己创建package.json文件
2、然后新建bin目录,用来存放执行文件,template目录用来存放项目模板

最后目录树应该是这样

  • /bin
    • dva-native.js
  • /template
    • test
    • android
    • app
    • ios
    • app.json
    • index.js
    • jsconfig.json
    • package.json
    • README.md
  • packge.json
3、命令行工具,顾名思义,最重要的就是在终端输入的命令,以及如何让系统能够识别
  • 如何让npm识别命令? 先在package.json文件里面增加以下字段
"bin": {
    "dva-native": "./bin/dva-native.js"
  }

dva-native即为我们需要识别的命令,最好不要跟其他命令行工具重名该字段后面的值意思就是执行dva-native后需要运行的代码 值得一提的是,dva-native .js文件开头需要使用 ! /usr/bin/env node来指定使用node执行该文件

好了,最后在命令行下执行npm link,再输入dva-native就能被识别了

  • 如何获得命令行下输入的参数?

很幸运,node可以很轻易的实现,使用process.argv即可,但是我们一般使用process.argv.slice(2) 前两个参数为node所在地址和正在执行文件的地址,对于本教程来说,就是dva-native.js,除此之外我们需要获得命令执行的位置,可以使用process.cwd()获得 然后我们就可以去实现 dva-native -v dva-native --help dva-native new app等命令了,第一个读取package.json里面的name字段打印即可,第二个捕获–help参数根据自己需要打印提示信息就OK了。

4、示例如下:
if (argv[0] === "--help") {
     console.log("Use dva-native new project to create a new project!");
     return;
   }
   if (argv[0] === "-v") {
     console.log(require("../package.json").version);
     return;
   }
 }
 if (argv[0] !== "new") {
   console.warn(
     `You should use dva-native new to create you app, do not use dva-native ${
     argv[0]
     }`
   );
   return;
 }

最后一个就是最重要的了,执行这个命令后需要在命令执行所在的地址下新建app文件夹,并将template文件夹下的所有文件拷贝过去,但是对于一个友好的cli工具来说,如果用户需要新建的文件夹已存在,需应该示用户是否需要覆盖该文件夹

5、如何进行命令行交互呢?

node又提供了一个强有力的模块readline 直接引入const readline = require("readline"); 当目标文件夹已存在时,执行以下代码

const terminal = readline.createInterface({
   input: process.stdin,
   output: process.stdout
 });
terminal.question(
     "Floder is already exits, do you want override it ? yes/no ",
     answer => {
       if (answer === "yes" || answer === "y") {
         resolve(true);
         terminal.close();
       } else {
         process.exit();
       }
     }
   );

当用户允许覆盖时,返回true,然后停止接受命令行的输入,否则退出进程

6、 如何拷贝整个文件夹?
function traverse(templatePath, targetPath) {
  try {
    const paths = fs.readdirSync(templatePath);
    paths.forEach(_path => {
      const _targetPath = path.resolve(targetPath, _path);
      const _templatePath = path.resolve(templatePath, _path);
      console.log("creating..." + _targetPath);
      if (!fs.statSync(_templatePath).isFile()) {
        fs.mkdirSync(_targetPath);
        traverse(_templatePath, _targetPath);
      } else {
        copyFile(_targetPath, _templatePath);
      }
    });
  } catch (error) {
    console.log(error);
    return false;
  }
  return true;
}
function copyFile(_targetPath, _templatePath) {
  fs.writeFileSync(_targetPath, fs.readFileSync(_templatePath), "utf-8");
  //fs.createReadStream(_targetPath).pipe(fs.createWriteStream(_templatePath));
}

由于后面我们需要执行npm intsall安装模块依赖,所以我们需要知道什么时候拷贝完成了,所以读取文件目录,写入文件均采用同步方式执行。使用readdirSync遍历目录,如果是目录,递归,否则进行拷贝操作,这里使用trycatch是为了捕获错误,有可能该文件夹下node并没用权限去进行操作,最后拷贝完成返回true

7、最后,如何执行上文提到的npm install呢?

提示用户自己输入吗?显然不是,这里我们借助const exec = require("child_process").exec;来完成命令执行

function installDependencies(projectName) {
  console.log("\ninstalling...");
  if (
    shell.exec(
      `cd ${projectName} && npm install --registry=http://registry.cnpmjs.org`
    ).code
  ) {
    if (shell.exec(`cd ${projectName} && npm install`).code) {
      console.log(colors.red("install dependencies failed!"));
    } else {
      console.log(
        colors.yellow(
          [
            `Success! Created ${projectName} at ${cwdPath}.`,
            "Inside that directory, you can run several commands and more:",
            "  * npm start: Starts you project.",
            "  * npm test: Run test.",
            "We suggest that you begin by typing:",
            `  cd ${projectName}`,
            "  npm start",
            "Happy hacking!"
          ].join("\n")
        )
      );
    }
  }
}

其实还可以使用swapn来完成,区别参考https://www.cnblogs.com/xiaoniuzai/p/6889164.html,上述的文件拷贝操作也可以借助swapn来完成,只不过mac和windows下的操作有点不一样我就没有采用了,具体使用参考https://github.com/dvajs/dva-cli/blob/master/src/install.js#L3

至此,一个简单的cli工具已编写完成,但是我们没有考虑生成途中用户使用^C退出后该如何进行处理

完整代码见github
回到顶部