Шаг за шагом, чтобы завершить node-cli

Node.js внешний интерфейс

node-cli — это инструмент, использующий nodejs для взаимодействия с оболочкой и выполнения указанной работы. Обычно они выглядят так:

sass xx.scss:xx.css
webpack ....

Подождите, мы внедрили этот инструмент, чтобы вытягиватьCavinHuang/webpack-multi-skeleton многостраничный скелет веб-пакетаИнструмент поддержки для локального и быстрого создания проектов, представленный следующими командами:

webpack-template i  # install git端所有的模板列表供选择,选择其中之一后进行本地缓存
webpack-template init # 通过一些选项,初始化整个项目

Вся идея наверное такая, начнем с самого простого и реализуем его шаг за шагом.

Реализация собственного узла команды

Мы инициализируем проект напрямую с помощью npm init

npm init node-cli-demo

Полностью да, входим в проект, добавляем следующий код в package.json

"bin": {
  "hi": "./bin/hi.js"
},

Создайте каталог bin и hi.js и напишите следующий код в hi.js.

#!/usr/bin/env node
console.log('Hi Welcome node cli')

Используйте командную строку для входа в текущий каталог проекта и введите

hi

Если нет такого командной строки, введите

npm link

First, we all know that the operating system will have a PATH environment variable, when the system call a command and they will find the path registered in the PATH variable, if the path is called a registered there, otherwise it could not find the command быстрый.我们可以通过process.env获取本机系统中所有的环境变量,所以这句话主要是帮助脚本找到node的脚本解释器,可以理解为调用系统中的node来解析我们的脚本。

Обработка аргументов командной строки

Объект процесса узла — это глобальный объект, который предоставляет информацию и управление текущим процессом Node.js и может вызываться без require() в среде узла.

Свойство process.argv возвращает массив, содержащий аргументы командной строки, переданные при запуске процесса Node.js. Первый элемент — это process.execPath, если вам нужно получить доступ к необработанному значению argv[0], вы можете использовать process.argv0, вторым элементом будет путь к файлу JavaScript для выполнения, а остальные элементы будут любыми. другие аргументы командной строки.

#!/usr/bin/env node
console.log('call %s', process.argv[2]);

Затем введите test hello и распечатайте call hello.

Для обработки параметров командной строки мы используем готовый модуль commander, который предоставляет мощные функции для пользовательского ввода командной строки и разбора параметров. Здесь мы используем легкий, выразительный командор для обработки.

Официальный сайт:официальный сайт командира

См. пример с официального сайта

#!/usr/bin/env node
var program = require('commander');

program
  .version('0.1.0')
  .option('-C, --chdir <path>', 'change the working directory')
  .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
  .option('-T, --no-tests', 'ignore test hook');

program
  .command('setup [env]')
  .description('run setup commands for all envs')
  .option("-s, --setup_mode [mode]", "Which setup mode to use")
  .action(function(env, options){
    var mode = options.setup_mode || "normal";
    env = env || 'all';
    console.log('setup for %s env(s) with %s mode', env, mode);
  });

program
  .command('exec <cmd>')
  .alias('ex')
  .description('execute the given remote cmd')
  .option("-e, --exec_mode <mode>", "Which exec mode to use")
  .action(function(cmd, options){
    console.log('exec "%s" using %s mode', cmd, options.exec_mode);
  }).on('--help', function() {
    console.log('  Examples:');
    console.log();
    console.log('    $ deploy exec sequential');
    console.log('    $ deploy exec async');
    console.log();
  });

program
  .command('*')
  .action(function(env){
    console.log('deploying "%s"', env);
  });

program.parse(process.argv);

Первая установка командира

yarn add commander # OR npm install commander

Взгляните на эффект:

hi -V

hi setup

hi exec

commander.js API

  • Option() ——> Инициализировать объект пользовательского параметра, установить «Ключевое слово» и «Описание»
  • Command() ——> Инициализировать объект параметра командной строки, напрямую получить ввод командной строки и вернуть массив или строку
  • Команда команды # () -> Определите имя команды
  • Command#arguments() ——> Определить параметры исходной команды
  • Command#parseExpectedArgs() ——> анализировать ожидаемые аргументы
  • Command#action() ——> Зарегистрировать функцию обратного вызова команды
  • Command#option() ——> Для определения параметров необходимо задать «ключевое слово» и «описание».Ключевое слово включает в себя «аббревиатуру» и «полный текст», разделенные «,», «|», «пробел».
  • Command#allowUnknownOption() ——> Разрешить неизвестные параметры командной строки
  • Command#parse() ——> parse process.argv, вызывать команду при установке опций и определений
  • Команда #parseOptions() -> парсить параметры
  • Команда # OPTS () -> Установленные параметры
  • Команда # Описание () -> Добавить команду Описание
  • Command#alias() ——> установить псевдоним команды
  • Command#usage() ——> установить/получить использование
  • Command#name()
  • Command#help()

program
    .command( 'list' ) //声明hi下有一个命令叫list
    .description( 'list files in current working directory' ) //给出list这个命令的描述
    .option( '-a, --all', 'Whether to display hidden files' ) //设置list这个命令的参数
    .action( function ( options ) { //list命令的实现体
        var fs = require( 'fs' );
        //获取当前运行目录下的文件信息
        fs.readdir( process.cwd(), function ( err, files ) {
            var list = files;
            if ( !options.all ) { //检查用户是否给了--all或者-a的参数,如果没有,则过滤掉那些以.开头的文件
                list = files.filter( function ( file ) {
                    return file.indexOf( '.' ) !== 0;
                } );
            }
            console.log( list.join( '\n\r' ) ); //控制台将所有文件名打印出来
        } );
    } );

бегать

hi list # hi list -a 或者 --all来查看效果

портал github, ветка 0.0.1 — это код первой версии

Создайте официальную версию среды разработки для поддержки синтаксиса es6 и eslint.

yarn add -D babel-cli babel-eslint babel-plugin-transform-es2015-modules-commonjs babel-preset-latest-node

Создайте новый .babelrc в корневом каталоге проекта со следующим содержимым:

{
  "presets": [
    ["env", {
      "targets": {
        "node": "current"
      }
    }]
  ],
  "plugins": [
    "transform-es2015-modules-commonjs"
  ]
}

Новый каталог SRC, для разработки, новый каталог SRC / командной команды и каталог SRC / UTILS, используйте для разработки. После создания структуры каталога выглядит следующим образом:

├─bin             # 脚本启动文件所在目录
├─node_modules    # libraray 目录
│  └─commander    
│      └─typings  
└─src             # 开发目录
    ├─command     # 命令实现目录,一个命令对应一个文件
    └─utils       # 工具目录

Далее реализуем запись и переносим функцию в соответствующий файл реализации команды для конкретной реализации. Создайте новый index.js для обработки записи, а затем создайте src/index.js для фактической переадресации функций.Содержимое index.js выглядит следующим образом.

// babel解析
require( "babel-register" )
require( "babel-core" )
    .transform( "code", {
        presets: [ [ require( 'babel-preset-latest-node' ), {
            target: 'current'
        } ] ]
    } );
require( 'babel-polyfill' )

require('./src')

Содержимое src/index.js выглядит следующим образом:

var program = require( 'commander' );
program.parse( process.argv ); //开始解析用户输入的命令
require( './command/' + program.args + '.js' ) // 根据不同的命令转到不同的命令处理文件

Объясните, почему я хочу это сделать:


  • Затем мы можем создать соответствующую запрашиваемую цену, src/command/init.js src/command/install.js два файла обработки команд, содержимое которых выглядит следующим образом:

src/command/list.js:

var program = require( 'commander' );
program
    .command( 'init' )
    .description( 'init project for local' )
    .action( function ( options ) { //list命令的实现体
        // to do
        console.log( 'init command' );
    } );
program.parse( process.argv ); //开始解析用户输入的命令

src/command/install.js:

var program = require( 'commander' );
program
    .command( 'install' )
    .description( 'install github project to local' )
    .action( function ( options ) { //list命令的实现体
        // to do
        console.log( 'install command' );
    } );
program.parse( process.argv ); //开始解析用户输入的命令

Введите следующую команду в командной строке для проверки:

webpack-template install

webpack-template init

Второе издание завершает кодовый адрес:[Адрес Github второго издания, вы можете клонировать попытки вниз]

Далее мы должны установить и запустить функцию инициализации. Сначала установите предусмотренные шаги:

  • Вытащите шаблон проекта на склад через github api
  • Скачать, выбрав шаблон
  • Кэшируется в локальный временный каталог для прямого использования в следующий раз

Во-первых, перейдите кgithub api v3Найдите нужный интерфейс API,
Чтобы облегчить использование шаблонов управления проектами, я создал новую организацию для управления. Итак, я в основном через

/orgs/:org/repos #获取项目
和
/repos/:owner/:repo #获取版本

Проект был построен, вы можете просмотреть детали склада через следующий API 1. Список проектов

url -i https://api.github.com/orgs/cavinHuangORG/repos

2. Версия проекта

curl -i https://api.github.com/repos/cavinHuangORG/webpack-multipage-template/tags

Выбор параметров через командную строку имеет следующие последствия:


inquirer.gif

Здесь мы используем другую библиотеку взаимодействия с командной строкой, inquirer.js, которая в основном используется для выбора и ввода командной строки; сначала мы реализуем простой код в insatll.js для завершения следующего кода:

var inquirer = require( 'inquirer' );
program
    .command( 'install' )
    .description( 'install github project to local' )
    .action( function ( options ) { //list命令的实现体
        // to do
        console.log( 'install command' );
        let choices = [ 'aaa', 'bbb', 'ccc', 'dddd' ];
        let questions = [ {
            type: 'list',
            name: 'repo',
            message: 'which repo do you want to install?',
            choices
  } ];
        // 调用问题
        inquirer.prompt( questions )
            .then( answers => {
                console.log( answers ); // 输出最终的答案
            } )
    } );
program.parse( process.argv ); //开始解析用户输入的命令

Окончательный результат выглядит следующим образом:


install-2.gif

Мы хотим, чтобы этот эффект был почти завершен. Далее я хотел бы ввести некоторые конкретные параметры пользователем для инициализации всего проекта.

download-git-repo

Далее мы будем использовать библиотеку для загрузки кода библиотеки github,download-git-repo, использование выглядит следующим образом:

download(repository, destination, options, callback)

Загрузите репозиторий git в папку назначения с параметрами и обратным вызовом.

  • адрес библиотеки репозитория github

    • GitHub — github:владелец/имя или просто владелец/имя
    • GitLab - gitlab:owner/name
    • Bitbucket - bitbucket:owner/name
  • папка назначения

  • Параметры опций для несут при загрузке

const downloadGitRepo = require('download-git repo')
// 把目标项目下载到当前目录下的test下
downloadGitRepo('CavinHuang/node-cli-demo', './test', false, err => {
  console.log(err ? 'SUCCESS' : "FAIL");
} )

Мы выделяем класс для получения списка git-репозитория, информации о версии, загрузки git-кода и других операций. В основном существуют следующие методы. Код не будет опубликован. Весь код находится в ветке 0.0.3 git-репозитория.

/**
 * 获取git仓库列表
 */
async fetchRepoList() {}

/**
 * 获取仓库所有的版本
 * @param  {[string]} repo [仓库名称]
 * @return {[type]}      [description]
 */
async fetchRepoTagList( repo ) {}

/**
 * 获取仓库详细信息
 * @param  {[string]} repo [仓库名称]
 * @return {[type]}      [description]
 */
async fetchGitInfo( repo ) {}

/**
 * 下载git仓库代码到指定文件夹
 * @param  {[string]} repo [仓库名称]
 * @return {[type]}      [description]
 */
async downloadGitRepo( repo ) {}

В install.js для начала нам нужно вытащить все шаблоны на складе для выбора, просто заменить выборы на полученный через апи список гит штанов.

import gitCtrl from '../utils/gitCtrl'
import config from '../config'
// 初始化git操作类
let git = new gitCtrl.gitCtrl( config.repoType, config.registry )

Изменение в действии:

// 获取git仓库列表
let choices = await git.fetchRepoList();

Далее следует загрузить код на локальный сервер в соответствии с выбором хранилища пользователем.Мы создаем новую папку конфигурации для хранения некоторых конфигураций, определяем некоторые часто используемые переменные, такие как каталог кеша, версия и т. д., и создаем новую константу. .js

const os = require( 'os' );
import {
    name,
    version,
    engines
} from '../../package.json';

// 系统user文件夹
const home = process.env[ ( process.platform === 'win32' ) ? 'USERPROFILE' : 'HOME' ];

// user agent
export const ua = `${name}-${version}`;

/**
 * 文件夹定义
 * @type {Object}
 */
export const dirs = {
    home,
    download: `${home}/.webpack-project`,
    rc: `${home}/.webpack-project`,
    tmp: os.tmpdir(),
    metalsmith: 'metalsmith'
};

/**
 * 版本
 * @type {Object}
 */
export const versions = {
    node: process.version.substr( 1 ),
    nodeEngines: engines.node,
  [ name ]: version
};

index.js

/**
 * 配置文件
 */

export default {
    registry: 'cavinHuangORG', // 仓库地址
    repoType: 'org', // ['org', 'user']
    metalsmith: true
}

С ними давайте загрузим код ниже:

// 下载库
let result = await git.downloadGitRepo( answers.repo )
console.log( result ? 'SUCCESS' : result )

В этот момент мы бежим

webpack-template install

Результат выглядит следующим образом:


install-2.gif

Далее добавляем выбор версии, немного модифицируем код в install.js и добавляем выбор версии:

// 取出选择的git仓库
const repo = answers.repo;
// 获取选择仓库所有的版本
const tags = await git.fetchRepoTagList( repo );

if ( tags.length === 0 ) {
  version = '';
} else {
  choices = tags.map( ( {
    name
  } ) => name );

  answers = await inquirer.prompt( [
    {
      type: 'list',
      name: 'version',
      message: 'which version do you want to install?',
      choices
  }
] );
  version = answers.version;
}
console.log( answers ); // 输出最终的答案
let result = await git.downloadGitRepo( [ repo, version ].join( '@' ) );
console.log( result ? 'SUCCESS' : result )
install-3.gif

В это время заходим в .webpack-project в пользовательской папке системы, и находим замененный нами проект.
На данный момент наш код установки завершен,гитхаб-адрес

выполнить команду инициализации

Команда init инициализирует локальный проект, включая некоторую информацию, заполненную пользователем, по сути, принцип заключается в замене включенных параметров и копировании проекта, загруженного в каталог кеша, в текущий каталог выполнения командной строки. Прежде всего, мы все еще завершаем доход простейшей командной строки, ввод информации пользователем, здесь мы все еще используем опрашиватель для завершения:

// 1、选择哪个模板
// 2、当前项目的名字,也是初始化项目的文件夹名字
let questions = [
  {
    type: 'list',
    name: 'template',
    message: 'which template do you want to init?',
    choices: list
  }, {
    type: 'input',
    name: 'dir',
    message: 'project name',
    async validate( input ) {
      // 下面这行代码用于通知异步任务
      const done = this.async();
      if ( input.length === 0 ) {
        done( 'You must input project name' );
        return;
      }
      const dir = resolve( process.cwd(), input );
      if ( await exists( dir ) ) {
        done( 'The project name is already existed. Please change another name' );
      }
      done( null, true );
    }
  }
];
const answers = await inquirer.prompt( questions )

справка по использованию ncp

Далее следует собрать более подробную информацию и скопировать загруженные файлы во временный каталог для обработки.Здесь для копирования файлов используется зрелая библиотека ncp, которая представляет собой библиотеку, совместимую с командным интерфейсом linux cp.Официальный сайт, основной метод вызова:ncp [source] [dest] [--limit=concurrency limit] [--filter=filter] --stopOnErr

var ncp = require('ncp').ncp;

ncp.limit = 16;

ncp(source, destination, function (err) {
 if (err) {
   return console.error(err);
 }
 console.log('done!');
});

var mkdirp = require('mkdirp');

mkdirp('/tmp/foo/bar/baz', function (err) {
    if (err) console.error(err)
    else console.log('pow!')
});

По этим двум библиотекам мы устанавливаем инструментальную функцию, специально используемую для копирования нашего проекта

import {
    ncp
} from 'ncp';
import mkdirp from 'mkdirp'
import {
    exists
} from 'mz/fs'
export default function copyFile( src, dest ) {
    return new Promise( async ( resolve, reject ) => {
        if ( !( await exists( src ) ) ) {
            mkdirp.sync( src ); //异步创建
        }
        ncp( src, dest, ( err ) => {
            if ( err ) {
                reject( err );
                return;
            }
            resolve();
        } );
    } );
}

Скопируйте во временную папку и сгенерируйте проект, пройдите процесс заполнения данных, который в основном использует генератор статических сайтов (Metalsmith) и глоток иКонсолидация библиотеки слияния шаблона двигателя

Добавить действие копирования и действие компиляции в init.js

const answers = await inquirer.prompt( questions )
const metalsmith = config.metalsmith;
if ( metalsmith ) {
  const tmp = `${dirs.tmp}/${answers.template}`;
  // 复制一份到临时目录,在临时目录编译生成
  await copyFile( `${dirs.download}/${answers.template}`, tmp );
  await metalsmithACtion( answers.template ); // 根据参数编译
  await copyFile( `${tmp}/${dirs.metalsmith}`, answers.dir );
  await rmfr( tmp ); // 清除临时文件夹
} else {
  await copyFile( `${dirs.download}/${answers.template}`, answers.dir );
}

Наконец, все структура каталога выглядит следующим образом:

│  .babelrc
│  .gitignore
│  index.js
│  package.json
│  
├─bin
│      hi.js
│                      
└─src
    │  index.js
    │  
    ├─command
    │      init.js
    │      install.js
    │      
    ├─config
    │      constant.js
    │      index.js
    │      
    └─utils
            copyFile.js
            gitCtrl.js
            initProjectQuestion.js    #初始化项目的问题
            metalsmithACtion.js       #临时文件夹编译动作
            render.js                 #编译模板的插件

Здесь реализованы все функции, чтобы сделать всю команду более удобной, более процессной, вводим эту библиотеку, адрес проекта:ora, основные эффекты следующие:

Создайте новый OraLoading.js в utils

import ora from 'ora';

export default function OraLoading( action = 'getting', repo = '' ) {
    const l = ora( `${action} ${repo}` );
    return l.start();
}

Ну тут все написали, попробуем эффект: Первое установить


install-last.gif

Init тоже одинаково, нет демонстрации

публиковать нпм

  • Перейдите на npm.com, чтобы зарегистрировать свою учетную запись, перейдите в папку текущего каталога из командной строки, выполните команду npm login, введите пароль своей учетной записи и войдите в систему.

воплощать в жизнь

npm publish .

Вы можете опубликовать свой собственный пакет npm, обратите внимание на яму здесь, если вы используете исходный код Taobao, вам нужно переключиться обратно на исходный код npm,

npm config set registry http://registry.npmjs.org

В противном случае проверка не проходит.

Наконец, укажите адрес кода завершения github:гитхаб-портал