Как создать реальную программу Node CLI с помощью Node

1656604097 kak sozdat realnuyu programmu node cli s pomoshhyu node

от Timber.io

9ZjKi9cAzlM0d4FWrOmGLwpdnAyG2hCKpvlT

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

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

U7W2bAC42fKLJFVZ4YDEcjOfhEGR-68myb0T

Примечание: Есть несколько библиотек, которые помогают создавать сложные интерфейсы командной команды, такие как oclif, yargs и commander, но мы сохраним наши зависимости тонкими для этого примера, чтобы вы могли лучше понять, как все работает под капотом. Этот учебник предусматривает, что у вас есть базовые рабочие знания JavaScript и Node.

Начинаем

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

{
  "name": "outside-cli",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {},
  "devDependencies": {},
  "dependencies": {}
}
package.json
module.exports = () => {
  console.log('Welcome to the outside!')
}
index.js

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

#!/usr/bin/env node
require('../')()
урна/снаружи

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

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

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

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

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

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

Когда вы запускаете программу CLI, она состоит из аргументов и команд. Аргументы (или «флаги») – это значение, перед которыми стоит один или два дефиса (например, -d, --debug или --env production) и полезны для передачи параметров в нашу программу. Команды — это все другие значения, не имеющие флажка.

В отличие от команд аргументы не нужно указывать в определенном порядке. Например, мы могли бы бежать outside today Brooklyn и просто допустим, что вторая команда всегда будет расположением, но не лучше ли было бы запустить outside today --location Brooklyn если мы хотим добавить больше опций в будущем?

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

npm install --save minimist
const minimist = require('minimist')

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

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

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

Синтаксис аргумента

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

Это важно помнить значения должны быть в кавычках, если они содержат специальные символы или пробел. Бег --foo bar baz даст вам `{: [‘baz’]foo: ‘бар’ }, but running—foo «bar baz»would give you{ foo: ‘bar baz’}`._

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

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
  }
}
index.js
module.exports = (args) => {
  console.log('today is sunny')
}
cmds/today.js

Теперь, если ты бежишь outside todayвы увидите сообщение «сегодня солнечно», а если вы забежите outside foobar, он скажет вам, что «foobar» не является настоящей командой. Нам еще нужно спрашивать API погоды, чтобы получить реальные данные, но это хорошее начало.

Есть несколько команд и аргументов, ожидаемых в каждом CLI: help, --help и -hкоторый должен показывать справочные меню, и version, --version и -v который должен выводить текущую версию программы. Мы также должны по умолчанию использовать главное меню справки, если не указана ни одна команда.

Это можно легко реализовать в наших текущих настройках, добавив два случая к нашему оператору switch, значение по умолчанию для cmd переменной и некоторые операторы if для флагов аргументов справки и версии. Minimist автоматически анализирует аргументы на ключ/значение, поэтому выполняется outside --version сделает args.version равноправно.

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 команда.

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

module.exports = (args) => {
  console.log(`v${version}`)
}
cmds/version.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)
}
cmds/help.js

Теперь, если ты бежишь outside help today или outside today -hвы должны увидеть меню справки для today команда. Бег outside или outside -h должно показать главное меню справки.

G1BNtKcKV5D1dMaVgKfclg7mZ1IYF8WXps9w

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

module.exports = (args) => {
  console.log('tomorrow is rainy')
}
cmds/forecast.js
// ...
    case 'forecast':
      require('./cmds/forecast')(args)
      break
// ...
index.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`,
}

// ...
cmds/help.js

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

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

npm install --save axios ora

Получение данных по API

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

Примечание: API Yahoo использует синтаксис «YQL», и это немного удивительно – не пытайтесь понять его, просто скопируйте и вставьте. Это был единственный API погоды, который я мог найти, не требующий ключа API.

const axios = require('axios')

module.exports = async (location) => {
  const results = await axios({
    method: 'get',
    url: '
    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
}
utils/weather.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)
  }
}
cmds/today.js

Теперь, если ты бежишь outside today --location "Brooklyn, NY"во время выполнения запроса вы увидите быстрое вращение и текущие погодные условия.

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

0eoDghnUZOOvNti5wf3puX9oyxvK4RMjYG10

Прекрасно! Теперь скопируем этот код в наш forecast команду и немного измените форматирование.

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)
  }
}
cmds/forecast.js

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

const axios = require('axios')

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

  const { city, region } = results.data
  return `${city}, ${region}`
}
utils/location.js
// ...
const getLocation = require('../utils/location')

module.exports = async (args) => {
  // ...
    const location = args.location || args.l || await getLocation()
    const weather = await getWeather(location)
  // ...
}
cmds/today.js & cmds/forecast.js

Теперь, если вы просто бежите outside forecast без местоположения вы увидите прогноз для вашего текущего местоположения.

oXE-zFoeG9LjeDVfp-n7vhSs3KZuC3tue3-m

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

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

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

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

module.exports = (message, exit) => {
  console.error(message)
  exit && process.exit(1)
}
utils/error.js
// ...
const error = require('./utils/error')

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

Заканчиваем

Последний шаг к тому, чтобы вывести нашу библиотеку в дикую природу – это опубликовать ее в менеджере пакетов. Поскольку наше приложение написано на JavaScript, имеет смысл опубликовать его в NPM. Давайте заполняем нашу package.json чуть больше:

{
  "name": "outside-cli",
  "version": "1.0.0",
  "description": "A CLI app that gives you the weather forecast",
  "license": "MIT",
  "homepage": "
  "repository": {
    "type": "git",
    "url": "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 install --save а не npm install --global.

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

Резюме

Это структура, которую мы придерживаемся для всех наших программ CLI здесь, в Timber, и она помогает сохранять организованность и модульность.

Некоторые ключевые выводы из этого учебника для тех, кто только просмотрел его:

  • Файлы Bin являются точкой входа для любой программы CLI и должны вызывать только главную функцию.
  • Файлы команд не нужны, пока они не понадобятся
  • Всегда включать help и version команды
  • Сохраняйте командные файлы тонкими – их основная цель – вызвать функции и показывать сообщения пользователя
  • Всегда показывайте какой-либо индикатор активности
  • Выйдите с правильными кодами ошибок

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

Мы являемся облачной лесозаготовительной компанией здесь @ Timber. Мы были бы рады, если бы вы испытали наш продукт (это действительно здорово! — вы можете создать бесплатную учетную запись здесь), но это все, что мы собираемся рекламировать наш продукт… вы пришли сюда, чтобы узнать о создании программы CLI в Node, и , надеюсь, это руководство помогло вам начать работу.

Первоначально опубликовано на timber.io.

Добавить комментарий

Ваш адрес email не будет опубликован.