node之手写脚手架

本文档详细介绍了如何从零开始创建一个命令行界面工具(CLI),包括初始化CLI工具,动态获取版本号,自定义配置命令,配置create命令以拉取并选择GitHub模板,以及下载和处理模板的过程。通过这个过程,读者可以了解到如何利用Node.js和相关库构建一个功能丰富的CLI工具。
摘要由CSDN通过智能技术生成

一、初始化cli工具

(1)创建文件

  1. 创建一个文件夹
npm init -y
  1. 修改 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"
  }
}

  1. 添加之后说明需要在根目录下创建一个bin文件夹,创建welkin.js文件作为入口文件
//node环境使用
`#! /usr/bin/env node


//下面这一行需要创建main文件
require('../src/main.js');

  1. 创建src/main.js入口文件
console.log('我是测试文件');

(2)执行文件

  1. 链接包到全局下使用
npm link

这个地方需要sudo输入密码,且需要上面package.json中指定文件夹仔细

  1. 执行
welkin-cli

输出:
在这里插入图片描述

代表创建完成

二、动态获取版本号

(1)创建

  1. 下载自定义输出工具
npm install commander

npm地址:https://www.npmjs.com/package/commander

介绍几个常用的方法如下:

  • version:设置version
  • parse:解析用户传递过来的参数
  • command:设置自定义命令
  • option:可选参数 (选项)
  • alias:匹配
  • description:命令描述
  • action:执行对应命令后所执行的方法。
  1. 获取根目录下的package.json中的版本信息
const { version } = require('../package.json');
module.exports = {
  version,
};

  1. 入口文件中使用命令输出
//解析输入参数
const program = require('commander');
const { version } = require('./constants');
program.version(version).parse(process.argv);v

(2)使用

命令行输入

welkin-cli -V

在这里插入图片描述

可以看到和上面的package.json文件中的版本信息一模一样

三、自定义配置命令

(1)配置reflect

  1. 配置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: [],
  },
};

  1. 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. 这个地方需要判断创建的目录是否已经存在,存在则提示并创建失败
  2. 模板下载自定义目录有个好处就是可以缓存下来,当下一次创建项目时,如果模板一样就可以不用下载,可以走缓存

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的文件之后就会出现以下显示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rQbcgT3v-1631009753316)(/Users/didi/Library/Application Support/typora-user-images/image-20210907180610971.png)]

并在你创建的这个项目下创建一个自己配置的文件,类似下面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-luSOjXzd-1631009753317)(/Users/didi/Library/Application Support/typora-user-images/image-20210907180812739.png)]

参考文章:https://github.com/Tie-Dan/tdsp-cli
参考视频:https://www.bilibili.com/video/BV1w54y1B7Tb?p=6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值