«Перевод» на использование узла для создания приложения командной строки

Node.js внешний интерфейс Командная строка JavaScript

Создание приложений командной строки с помощью Node

существуетJavaScriptВ мире разработки приложениям командной строки еще не уделялось должного внимания. На самом деле, большинство инструментов разработки должны предоставлять интерфейс командной строки для таких разработчиков, как мы, а взаимодействие с пользователем должно быть сравнимо с хорошо созданным веб-приложением, таким как красивый дизайн, простые в использовании меню и понятные интерфейсы. ошибки Обратная связь, подсказки загрузки и индикаторы выполнения и т. д.

Существует не так много практических руководств, которые помогут нам использоватьNodeСоздайте интерфейс командной строки, поэтому эта статья будет началом, основанным на базовомhello worldкомандное приложение, шаг за шагом, чтобы построитьoutside-cli, который предоставляет текущую погоду и прогнозирует погоду где угодно в ближайшие 10 дней.

намекать: существует довольно много библиотек, которые могут помочь вам создавать сложные приложения командной строки, такие какoclif,yargsа такжеcommander, но для того, чтобы вы лучше понимали его обоснование, мы будем оставлять внешние зависимости как можно меньше. Конечно, мы предполагаем, что у вас уже естьJavaScriptа такжеNodeбазовые знания.

начиная

с прочимиJavaScriptКак и в случае с проектами, передовой практикой является созданиеpackage.jsonи пустой входной файл, который еще не требует никаких зависимостей, будьте проще.

package.json

{
  "name": "outside-cli",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {},
  "devDependencies": {},
  "dependencies": {}
}

index.js

module.exports = () => {
  console.log('Welcome to the outside!')
}

мы будем использоватьbinфайл для запуска этой новой программы, и он поместитbinФайл добавляется в системный каталог, чтобы его можно было вызывать из любого места.

#!/usr/bin/env node
require('../')()

Не видел его раньше#!/usr/bin/env node? Это называетсяshebang. информирует систему о том, что это неshellscript и указать, что следует использовать другой интерпретатор.

binФайл должен быть простым, потому что он предназначен только для вызова основной функции, и весь наш код должен быть размещен за пределами этого файла, чтобы обеспечить модульность и тестируемость, а также обеспечить возможность использования в будущем в другом коде. .

Чтобы иметь возможность запускать напрямуюbinфайл, нам нужно дать правильные права доступа к файлу, если вы находитесь вUNIXсреду, вам просто нужно выполнитьchmod +x bin/outside,WindowsПользователи могут полагаться только на себя, рекомендуется использоватьLinuxподсистема.

Далее мы добавимbinфайл вpackage.json, то когда мы устанавливаем этот пакет глобально (npm install -g outside-cli),binФайл автоматически добавляется в системный каталог.

package.json

{
  "name": "outside-cli",
  "version": "1.0.0",
  "license": "MIT",
  "bin": {
    "outside": "bin/outside"
  },
  "scripts": {},
  "devDependencies": {},
  "dependencies": {}
}

Теперь мы входим./bin/outside, Вы можете запустить его напрямую, приветственное сообщение будет напечатано, выполнить его в вашем корневом каталоге проектаnpm link, это создаст мягкую ссылку между системным путем и вашим двоичным файлом, так чтоoutsideКоманду можно запустить где угодно.

Приложение CLI состоит из параметров и директив, где параметры (или «флаги») — это значения с префиксом в один или два дефиса (например,-d,--debugили--env production), что очень полезно для приложений. Директивы относятся ко всем остальным значениям без флагов.

В отличие от директив, аргументы не требуют определенного порядка, например, запускoutside today Brooklyn, надо договориться, что вторая команда может представлять только регион, использовать--В противном случае запуститеoutside today --location Brooklyn, вы можете легко добавить дополнительные параметры.

Чтобы сделать приложение более практичным, нам нужно разобрать директиву и параметры, а затем преобразовать в литеральные объекты, мы можем использоватьprocess.argvсделать это вручную, но сейчас мы хотим установить первую зависимость проектаminimist, пусть это сделает это за нас.

npm install --save minimist

index.js

const minimist = require('minimist')

module.exports = () => {
  const args = minimist(process.argv.slice(2))
  console.log(args)
}

намекать:потому чтоprocess.argvПервые два аргумента — это интерпретатор и имя двоичного файла, поэтому мы используем.slice(2)Удалите первые два параметра и заботьтесь только о других переданных командах.

выполнить сейчасoutside todayбудет выводить{ _: ['today'] }. воплощать в жизньoutside today --location "Brooklyn, NY", выведет{ _: ['today'], location: 'Brooklyn, NY' }. Но теперь нам не нужно углубляться в использование параметров, подождите до фактического использованияlocationМы продолжим копать глубже в то время, и мы знаем пока достаточно, чтобы реализовать первую инструкцию.

синтаксис параметра

в состоянии пройтиэта статьяПомогает лучше понять синтаксис параметра. По сути, параметр может иметь один или два дефиса, за которыми следует соответствующее значение, которое по умолчанию равноtrue, параметры с одним дефисом также могут использовать сокращенную форму (-a -b -cили-abcсоответствовать{ a: true, b: true, c: true }).

Если значение параметра содержит специальные символы или пробелы, оно должно быть заключено в кавычки.. Например--foo barсоответствующий{ : ['baz'], foo: 'bar' },--foo "bar baz"вести переписку{ foo: 'bar baz' }.

Рекомендуется разделить код для каждой инструкции и загрузить его в память при вызове, что помогает сократить время запуска и избежать ненужных нагрузок. В основном коде команды просто используйтеswitchЭта практика может быть достигнута. В этой настройке нам нужно записать каждую директиву в отдельный файл и экспортировать функцию, и в то же время мы передаем аргументы каждой функции директивы для последующего использования.

index.js

const minimist = require('minimist')

module.exports = () => {
  const args = minimist(process.argv.slice(2))
  const cmd = args._[0]

  switch (cmd) {
    case 'today':
      require('./cmds/today')(args)
      break
    default:
      console.error(`"${cmd}" is not a valid command!`)
      break
  }
}

cmds/today.js

module.exports = (args) => {
  console.log('today is sunny')
}

Теперь, если вы выполнитеoutside today, вы увидите выводtoday is sunny, если выполняетсяoutside foobar, выведет"foobar" is not a valid command. Текущий прототип очень хорош, а дальше нам нужно получить реальные данные о погоде через API.

Есть некоторые команды и параметры, которые мы хотели бы включить в каждое приложение командной строки:help,--helpа также-hИспользуется для отображения списка помощи;--versionа также-vИспользуется для отображения информации о версии текущего приложения. Когда директива не указана, мы также должны отображать список помощи по умолчанию.

Minimistавтоматически проанализирует параметры как пары ключ-значение, поэтому запуститеoutside --versionсделаюargs.versionравныйtrue. Затем в программе установивcmdпеременная для сохраненияhelpа такжеversionРезультат оценки параметра, а затем вswitchВышеупомянутая функция может быть достигнута путем добавления двух операторов обработки к оператору.

const minimist = require('minimist')

module.exports = () => {
  const args = minimist(process.argv.slice(2))

  let cmd = args._[0] || 'help'

  if (args.version || args.v) {
    cmd = 'version'
  }

  if (args.help || args.h) {
    cmd = 'help'
  }

  switch (cmd) {
    case 'today':
      require('./cmds/today')(args)
      break

    case 'version':
      require('./cmds/version')(args)
      break

    case 'help':
      require('./cmds/help')(args)
      break

    default:
      console.error(`"${cmd}" is not a valid command!`)
      break
  }
}

При реализации новых директив формат должен быть таким же, какtodayИнструкции остаются прежними.

cmds/version.js

const { version } = require('../package.json')

module.exports = (args) => {
  console.log(`v${version}`)
}

cmds/help.js

const menus = {
  main: `
    outside [command] <options>

    today .............. show weather for today
    version ............ show package version
    help ............... show help menu for a command`,

  today: `
    outside today <options>

    --location, -l ..... the location to use`,
}

module.exports = (args) => {
  const subCmd = args._[0] === 'help'
    ? args._[1]
    : args._[0]

  console.log(menus[subCmd] || menus.main)
}

Теперь, если вы выполнитеoutside help todayилиoutside toady -h, ты увидишьtodayсправочная информация по команде, выполнитьoutsideилиoutside -hТо же самое справедливо.

Текущая настройка проекта приятна, потому что, когда вам нужно добавить новую директиву, вы просто создаете новый файл директивы и добавляете его вswitchоператор, а затем установите для него справочное сообщение.

cmds/forecast.js

module.exports = (args) => {
  console.log('tomorrow is rainy')
}

index.js

*// ...*
    case 'forecast':
      require('./cmds/forecast')(args)
      break
*// ...*

cmds/help.js

const menus = {
  main: `
    outside [command] <options>

    today .............. show weather for today
    forecast ........... show 10-day weather forecast
    version ............ show package version
    help ............... show help menu for a command`,

  today: `
    outside today <options>

    --location, -l ..... the location to use`,

  forecast: `
    outside forecast <options>

    --location, -l ..... the location to use`,
}

// ...

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

Для текущего приложения у нас нет возможности узнатьAPIпрогресс запроса, поэтому мы используем простойspinnerчтобы показать, что программа все еще работает. Затем мы устанавливаем две зависимости,axiosдля сетевых запросов,oraреализоватьspinner.

npm install --save axios ora

Получить данные из API

Теперь давайте создадим служебную функцию, которая использует Yahoo Weather API для получения данных о погоде в определенном регионе.

намекать: Yahoo API использует очень лаконичныйYQLГрамматика, нам не нужно специально ее понимать, просто скопируйте и используйте ее напрямую. Кроме того, это единственное, что я нашел, которому не нужно предоставлятьAPI keyAPI погоды.

utils/weather.js

const axios = require('axios')

module.exports = async (location) => {
  const results = await axios({
    method: 'get',
    url: 'https://query.yahooapis.com/v1/public/yql',
    params: {
      format: 'json',
      q: `select item from weather.forecast where woeid in
        (select woeid from geo.places(1) where text="${location}")`,
    },
  })

  return results.data.query.results.channel.item
}

cmds/today.js

const ora = require('ora')
const getWeather = require('../utils/weather')

module.exports = async (args) => {
  const spinner = ora().start()

  try {
    const location = args.location || args.l
    const weather = await getWeather(location)

    spinner.stop()

    console.log(`Current conditions in ${location}:`)
    console.log(`\t${weather.condition.temp}° ${weather.condition.text}`)
  } catch (err) {
    spinner.stop()

    console.error(err)
  }
}

теперь, когда вы выполняетеoutside today --location "Brooklyn, NY", сначала вы увидите быстро вращающийсяspinnerПоявится во время запроса из приложения, а затем отображается информация о погоде.

Когда запрос произойдет быстро, нам трудно увидеть индикатор загрузки. Если вы хотите искусственно замедлить скорость, вы можете добавить это предложение до функции инструмента погоды запроса:await new Promise(resolve => setTimeout(resolve, 5000)).

Превосходно! Затем мы копируем приведенный выше код для достиженияforecastкоманду, а затем просто измените выходной формат.

cmds/forecast.js

const ora = require('ora')
const getWeather = require('../utils/weather')

module.exports = async (args) => {
  const spinner = ora().start()

  try {
    const location = args.location || args.l
    const weather = await getWeather(location)

    spinner.stop()

    console.log(`Forecast for ${location}:`)
    weather.forecast.forEach(item =>
      console.log(`\t${item.date} - Low: ${item.low}° | High: ${item.high}° | ${item.text}`))
  } catch (err) {
    spinner.stop()

    console.error(err)
  }
}

теперь, когда вы выполняетеoutside forecast --location "Brooklyn, NY"После этого вы увидите результаты прогноза погоды на следующие 10 дней. Далее мы добавим вишенку на торт, когдаlocationВремя не указано, служебная функция, которую мы пишем для автоматического получения местоположения на основе IP-адреса.

utils/location.js

const axios = require('axios')

module.exports = async () => {
  const results = await axios({
    method: 'get',
    url: 'https://api.ipdata.co',
  })

  const { city, region } = results.data
  return `${city}, ${region}`
}

cmds/today.js & cmds/forecast.js

*// ...*
const getLocation = require('../utils/location')

module.exports = async (args) => {
  *// ...*
    const location = args.location || args.l || await getLocation()
    const weather = await getWeather(location)
  *// ...*
}

Теперь вы не добавляетеlocationПосле выполнения команды с параметрами вы увидите информацию о погоде, соответствующую текущему региону.

обработка ошибок

Мы не будем вдаваться в подробности наилучшего способа обработки ошибок в этой статье (в одном из последующих руководств), но самое главное — не забывать использовать правильный код выхода.

Если в вашем приложении командной строки произошла фатальная ошибка, вы должны использоватьprocess.exit(1), терминал воспримет, что программа выполнена не полностью, и сможет уведомить внешний мир через программу CI.

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

utils/error.js

module.exports = (message, exit) => {
  console.error(message)
  exit && process.exit(1)
}

index.js

*// ...*
const error = require('./utils/error')

module.exports = () => {
  *// ...*
    default:
      error(`"${cmd}" is not a valid command!`, true)
      break
  *// ...*
}

окончание

Последний шаг — опубликовать написанную нами библиотеку на удаленной платформе управления пакетами, поскольку мы используемJavaScript,NPMНе может быть более подходящим. Теперь нам нужно заполнить дополнительную информацию, чтобыpackage.jsonвнутри.

{
  "name": "outside-cli",
  "version": "1.0.0",
  "description": "A CLI app that gives you the weather forecast",
  "license": "MIT",
  "homepage": "https://github.com/timberio/outside-cli#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/timberio/outside-cli.git"
  },
  "engines": {
    "node": ">=8"
  },
  "keywords": [
    "weather",
    "forecast",
    "rain"
  ],
  "preferGlobal": true,
  "bin": {
    "outside": "bin/outside"
  },
  "scripts": {},
  "devDependencies": {},
  "dependencies": {
    "axios": "^0.18.0",
    "minimist": "^1.2.0",
    "ora": "^2.0.0"
  }
}
  • настраиватьengineгарантирует, что у пользователя есть более новаяNodeВерсия. Потому что мы использовали его напрямуюasync/await, поэтому мы требуемNodeВерсия должна быть 8.0 и выше.

  • настраиватьpreferGlobalВо время установки пользователю будет предложено указать, что библиотеку лучше всего устанавливать глобально, а не как локальную зависимость.

На этом пока все, теперь можно проходитьnpm publishОпубликовано на удаленном компьютере для загрузки другими пользователями. Если вы хотите пойти дальше, опубликуйте в других инструментах управления пакетами (например,Homebrew), ты можешь понятьpkgилиnexe, они помогут вам упаковать ваше приложение в автономный двоичный файл.

Суммировать

Структура каталогов кода, описанная в этой статье,TimberДалее следуют все приложения командной строки, указанные выше, и это помогает поддерживать организованность и модульность.

Для скорочтения мы также даем несколько советов для этого урока.ключевые выводы:

  • BinФайл является точкой входа всего приложения командной строки, и его ответственность заключается только в вызове основной функции.

  • Файл директивы не должен загружаться в основную функцию, когда она не выполняется.

  • всегда включатьhelpа такжеversionинструкция.

  • Файлы инструкций должны быть простыми, их основная обязанность — вызывать другие служебные функции, а затем отображать информацию для пользователя.

  • Всегда включайте некоторые инструкции по запуску для пользователя.

  • Приложение должно выйти с правильным кодом выхода.

Надеюсь, теперь вы лучше понимаете, как использоватьNodeСоздавайте и организуйте приложения командной строки. Эта статья только начало, дальше будем разбираться как оптимизировать дизайн, генерироватьascii artдобавление цвета и т.д. Исходный код этой статьи можно найти по адресуGitHubполучено выше.