Scotch.io научил меня многому из того, что я знаю в веб-разработке. Ранее в этом году я наткнулся на статью Роуленда Экемези Создание интерактивного приложения командной строки с помощью Node.js. Меня поразило количество знаний, которые я почерпнул из статьи. Я пришел к пониманию того, сколько из этих cli-приложений, таких как angular-cli, create-react-app, yeoman, npm, работает, поэтому я решил воспроизвести его работу и добавить в нее больше технологий.

В этой статье мы собираемся создать систему управления контактами из командной строки в Node.js, используя TypeScript, Google Cloud Functions и Firebase.

Технологии

  • Node.js
  • Машинопись
  • База данных Google в реальном времени (Firebase)
  • Облачные функции Google
  • Commander.js
  • Inquirer.js

Мы будем использовать commander.js для интерфейсов командной строки, inquirer.js для сбора входных данных, Node.js в качестве базовой инфраструктуры, Google Cloud Functions как FaaS (Function as a Service), который выполняет наши функции, и Firebase для сохранения данных.

Настройка проекта

Убедитесь, что у вас Node.js версии ≥ 6. Давайте создадим каталог проекта и инициализируем его как приложение Node.

mkdir contact-manager
cd contact-manager && npm init

Внедрение TypeScript в микс

Как было сказано ранее, мы будем использовать TypeScript вместо обычного JavaScript. TypeScript является строгим синтаксическим надмножеством JavaScript и добавляет в язык необязательную статическую типизацию.

npm i typescript -S
npm i -g ts-node

Как видите, мы установили typescript, а также установили ts-node глобально. ts-node - это исполняемый файл, который позволяет легко запускать TypeScript в среде Node.js.

ts-node contact.ts

Как вы можете видеть с ts-node, мы действительно можем запускать файлы TypeScript (* .ts) без предварительной компиляции их в простой JavaScript, а затем использовать node contact.js для запуска скомпилированного файла.

Настроить tsconfig.json

Наличие файла tsconfig.json в каталоге указывает, что каталог является корнем проекта TypeScript. Файл tsconfig.json определяет корневые файлы и параметры компилятора, необходимые для компиляции проекта. Проект компилируется одним из следующих способов:

Использование tsconfig.json

  • Вызывая tsc без входных файлов, и в этом случае компилятор ищет файл tsconfig.json, начиная с текущего каталога и продолжая вверх по цепочке родительских каталогов.
  • Вызов tsc без входных файлов и с параметром командной строки --project (или просто -p), который указывает путь к каталогу, содержащему файл tsconfig.json, или путь к допустимому файлу .json, содержащему конфигурации.

Когда входные файлы указаны в командной строке, tsconfig.json файлы игнорируются.

Сделайте так, чтобы ваш tsconfig.json выглядел так

{
"compilerOptions": {
 "target": "es5",
 "lib": [
   "es2017","es2015","dom","es6"
  ],
 "module": "commonjs",
 "outDir": "./",
 "sourceMap": false,
 "strict": true
},
"include": [
  "**.ts"
 ],
"exclude": [
  "node_modules",
  "firefunctions"
 ]
}

Установите зависимости нашего модуля npm

Для достижения нашей цели нам потребуются различные модули Node.

  • Axios - клиентская HTTP-библиотека.
  • Chalk - модуль узла, который позволяет разработчикам раскрашивать консольный вывод оболочки.
  • Commander - библиотека командной строки для Node.js.
  • Inquirer - набор общих интерактивных пользовательских интерфейсов командной строки.
  • Ora - счетчик терминала Node.js.
  • Core-js - Модульная стандартная библиотека для JavaScript. Он включает полифиллы для es6 и es8.
npm i axios -S
npm i chalk -S && npm i @types/chalk -D
npm i commander -S && npm i @types/commander -D
npm i inquirer -S && npm i @types/inquirer -D
npm i ora -S && npm i @types/ora -D
npm i core-js -S && npm i @types/core-js -D

После выполнения приведенных выше команд наш package.json будет выглядеть так.

{
 "name": "contact",
 "version": "1.0.0",
 "description": "",
  ...
 "devDependencies": {
 "@types/core-js": "^0.9.43",
 "@types/ora": "^1.3.1",
 "ts-node": "^3.3.0",
 "typescript": "^2.5.3"
 },
 "dependencies": {
 "@types/chalk": "^2.2.0",
 "@types/commander": "^2.11.0",
 "@types/inquirer": "0.0.35",
 "axios": "^0.16.2",
 "chalk": "^2.3.0",
 "commander": "^2.11.0",
 "core-js": "^2.5.1",
 "inquirer": "^3.3.0",
 "ora": "^1.3.0"
 }
}

Теперь TypeScript настроен в нашем приложении Node.js. Создадим файлы нашего проекта.

Создать файлы TypeScript

touch contact.ts
touch questions.ts
touch logic.ts
touch polyfills.ts

Каталог проекта contact-manager должен выглядеть так.

- contact-manager
  - /node_modules/
  - tsconfig.json
  - package.json
  - contact.ts /** the entry point of our app **/
  - questions.ts /** this contains arrays of questions **/
  - polyfills.ts /** this contains our app polyfills **/
  - logic.ts  /** this holds the logic of our app **/

Настроить полифиллы

Вернувшись к нашему tsconfig.json, вы увидите, что мы нацелены на es5, es6 и es8 в свойстве lib нашего tsconfig.json файла.

...
"target": "es5",
"lib": ["es2017", "es2015", "dom", "es6"],
...

Нам нужно настроить наш TS для использования библиотеки ES2107, а поскольку ES6 и ES8 еще не поддерживаются всеми браузерами, нам определенно нужен полифилл. core-js делает всю работу за нас. Мы установили core-js и его @types/core-js ранее, поэтому мы импортируем модуль core-js до загрузки нашего приложения. Поместим следующую строку в наш polyfills.ts файл.

/*** polyfills.ts ***/
//This file includes polyfills needed by TypeScript when using es2017, es6 or any above es5
// This file is loaded before the app. You can add your own extra polyfills to this file
import 'core-js'

contact.ts - это точка входа в наше приложение, поэтому мы открываем его и импортируем наш polyfills.ts файл.

/*** contact.ts ***/
import './polyfills'

Теперь мы готовы использовать любые функции ES8 или ES6. Раньше мы определяли логику нашего приложения. Давайте сначала настроим наши облачные функции и Firebase.

Что такое облачные функции для Firebase?

Облачные функции Firebase работают в размещенной, частной и масштабируемой среде Node.js, где вы можете запускать код JavaScript. Вы просто создаете реактивные функции, которые запускаются всякий раз, когда происходит событие. Облачные функции доступны как для Google Cloud Platform, так и для Firebase (они были созданы на основе Google Cloud Functions).

Создайте облачную функцию Firebase

Здесь мы будем использовать триггер HTTP. Посетите Google Cloud Platform, чтобы узнать больше о триггерах облачных функций. Прежде чем мы начнем создавать облачные функции, мы должны установить инструменты Firebase.

Установите Firebase CLI

Чтобы начать использовать облачные функции, нам понадобится Firebase CLI (интерфейс командной строки), установленный из npm. Если на вашем компьютере уже настроен Node, вы можете установить облачные функции с помощью:

npm install -g firebase-tools

Эта команда установит интерфейс командной строки Firebase глобально вместе со всеми необходимыми зависимостями Node.js.

Инициализировать проект

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

mkdir firefunctions
cd firefunctions

Чтобы инициализировать ваш проект:

  1. Запустите firebase login, чтобы войти в Firebase через браузер и аутентифицировать инструмент CLI.
  2. Наконец, запустите firebase init functions. Этот инструмент дает вам возможность устанавливать зависимости с NPM. Можно безопасно отказаться, если вы хотите управлять зависимостями другим способом.

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

-contact-manager
  -firefunctions/
  --+.firebaserc
  --+firebase.json
  --+functions/
  --+functions/package.json 
  --+functions/index.js
  --+functions/node_modules/
  -/node_modules/
  -tsconfig.json
  -package.json
  -contact.ts /** the entry point of our app **/
  -questions.ts /** this contains arrays of questions **/
  -polyfills.ts /** this contains our app polyfills **/
  -logic.ts  /** this holds the logic of our app **/
  • .firebaserc: скрытый файл, который помогает быстро переключаться между проектами с firebase use.
  • firebase.json: описывает свойства вашего проекта.
  • functions/: эта папка содержит весь код для ваших функций.
  • functions/package.json: файл пакета NPM, описывающий ваши облачные функции.
  • functions/index.js: основной источник кода ваших облачных функций.
  • functions/node_modules/: папка, в которой установлены все ваши зависимости NPM.

Теперь наши облачные функции настроены. Они написаны на простом JavaScript, но мы хотим написать его на TypeScript, а затем скомпилировать в JavaScript перед развертыванием в облаке.

Переименуйте index.js в index.ts, затем переместите в папку functions.

cd functions 

Установить машинописный текст

npm i typescript -S

Создать tsconfig.json

tsc init

Сделайте так, чтобы это выглядело так

/** firefunctions/functions/tsconfig.json **/
{
"compilerOptions": {
  "target": "es5",
  "lib": ["es2017", "es2015", "dom", "es6"],
  "module": "commonjs",
  "outDir": "./",
  "sourceMap": false,
  "strict": true
 },
 "include": [
  "index.ts"
 ],
 "exclude": [
  "node_modules"
 ]
}

Откройте package.json и измените раздел тегов scripts.

/** firefunctions/functions/package.json **/
...
"scripts": {
 "build": "tsc",
 "watch": "tsc -w",
 "deploy": " tsc && firebase deploy --only functions"
},
...

Импортируйте необходимые модули и инициализируйте приложение

Нам понадобятся два модуля узлов: Облачные функции и Модули Admin SDK (эти модули у нас уже установлены). Поэтому перейдите к index.ts и потребуйте эти модули, а затем инициализируйте экземпляр приложения администратора.

/** firefunctions/functions/index.ts **/
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase)
var contactsRef: admin.database.Reference = admin.database().ref('/contacts')

Закодируйте облачную функцию

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

  • добавить контакт
  • deleteContact
  • обновить
  • getContact
  • getContactList

Давайте создадим скелет следующих функций, перечисленных выше

/** firefunctions/functions/index.ts **/
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin';
...
exports.addContact = functions.https.onRequest(...)
exports.deleteContact = functions.https.onRequest(...)
exports.updateContact = functions.https.onRequest(...)
exports.getContact = functions.https.onRequest(...)
exports.getContactList = functions.https.onRequest(...)
...

В приведенном выше коде каждая из функций будет выполняться, когда соответствующие имена вызываются с использованием cURL, HTTP-запроса или URL-запроса из вашего браузера. Давайте попробуем базовую облачную функцию, чтобы увидеть, как она работает.

Реализация первой облачной функции

Откройте файл index.ts и вставьте следующую реализацию:

/** firefunctions/functions/index.ts **/
...
exports.helloWorld = functions.https.onRequest((request: express.Request, response: express.Response
) => {
    response.send("Hello from Firebase!");
});
...

Это самая простая форма реализации облачной функции Firebase на основе триггера HTTP. Облачная функция реализуется путем вызова метода functions.https.onRequest и передачи в качестве первого параметра функции, которая должна быть зарегистрирована для триггера HTTP.

Регистрируемая функция очень проста и состоит из одной строки кода:

response.send("Hello from Firebase!");

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

Чтобы опробовать эту функцию, нам нужно развернуть наш проект в Firebase.

npm run deploy

Примечание. Вышеупомянутый код компилирует index.ts в index.js, а затем развертывает файл JavaScript index.js.

Развертывание запущено, и вы должны получить следующий ответ:

Если развертывание было успешно завершено и вы получили обратно URL-адрес функции, который теперь можно использовать для запуска выполнения облачной функции. Просто скопируйте и вставьте URL-адрес в браузер, и вы должны увидеть следующий результат:

Примечание. Google Cloud Functions - это среда Node.js, что означает, что вы можете запускать npm install --save package_name и использовать в своих функциях любой пакет, который вам нужен.

Если вы открываете текущий проект Firebase в серверной части и нажимаете ссылку Функции, вы должны увидеть развернутую функцию helloWorld на панели инструментов:

Давайте добавим плоти нашим функциям.

/** firefunctions/functions/index.ts **/
import 'core-js'
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin';
import * as cors from 'cors'
import * as express from 'express'
admin.initializeApp(functions.config().firebase)
var contactsRef: admin.database.Reference = admin.database().ref('/contacts')
/**
* @function {addContact}
* @return {Object}
* @parameter {express.Request}, {express.Response}
**/
exports.addContact = functions.https.onRequest((request: any, response: any) => {
 cors()(request, response, () => {
  contactsRef.push({
   firstname: request.body.firstname,
   lastname: request.body.lastname,
   phone: request.body.phone,
   email: request.body.email
 })
})
 response.send({'msg': 'Done', 'data': {
   firstname: request.body.firstname,
   lastname: request.body.lastname,
   phone: request.body.phone,
   email: request.body.email
 }});
 
})
/**
* @function {getContactList}
* @return {Object}
* @parameter {express.Request}, {express.Response}
**/
exports.getContactList = functions.https.onRequest((request: any, response: any) => {
 contactsRef.once('value', (data) => {
  response.send({
   'res': data.val()
  })
 })
})
const app: express.Application = express();
app.use(cors({origin: true}))
app.put('/:id', (req: any, res: any, next: any) => {
 admin.database().ref('/contacts/' + req.params.id).update({
  firstname: req.body.firstname,
  lastname: req.body.lastname,
  phone: req.body.phone,
  email: req.body.email
})
 res.send(req.body)
 next()
})
app.delete('/:id', (req: any, res: any, next: any) => {
 admin.database().ref('/contacts/' + req.params.id).remove()
 res.send(req.params.id)
 next()
})
app.get('/:id', (req: any, res: any, next: any) => {
 admin.database().ref('/contacts/' + req.params.id).once('value'  (data) => {
  var sn = data.val()
  res.send({
   'res': sn
  })
  next()
  },(err: any) => res.send({res: err})
 )
})
/**
* @function {getContact}
* @return {Object}
* @parameter {express.Request}, {express.Response}
**/
exports.getContact = functions.https.onRequest((request: any, response: any) => {
 return app(request, response)
})
/**
* @function {updateContact}
* @return {Object}
* @parameter {express.Request}, {express.Response}
**/
exports.updateContact = functions.https.onRequest((request: any, response: any) => {
 return app(request, response)
})
/**
* @function {deleteContact}
* @return {Object}
* @parameter {express.Request}, {express.Response}
**/
exports.deleteContact = functions.https.onRequest((request: any, response: any) => {
 return app(request, response)
})

Вау… Мы много чего здесь сделали. Если вы заметили, для обработки запросов RESTful был задействован Express. Это возможно, потому что, как было сказано ранее, функция Google Cloud похожа на контейнер Docker со средой Node.js.

Разверните облачную функцию

Давайте развернем нашу облачную функцию. Выполните эту команду для развертывания:

npm run deploy

Мы закончили с нашими облачными функциями. Вернемся в папку менеджера контактов.

cd ../../

Определить логику приложения

В этом разделе мы определяем наши функции контроллера, которые обрабатывают ввод данных пользователем и вызывают соответствующую Облачную функцию.

/** logic.ts **/
import axios from 'axios'
import chalk from 'chalk'
import * as ora from 'ora'
const url: string = "https://us-central1-myreddit-clone.cloudfunctions.net"
export const addContact = (answers: any) => {
(async () => {
 try {
  const spinner = ora('Adding contact ...').start();
  let response = await axios.post(`${url}/addContact`,answers)
  spinner.stop()
  console.log(chalk.magentaBright('New contact added'))
 } catch (error) {
  console.log(error)
 }
 })()
}
...

По сути, мы определили URL-адрес нашей облачной функции, которая будет использоваться в зависимости от типа выполняемого действия. Axios используется для отправки запроса вместе с полезной нагрузкой и для получения ответа от нашей облачной функции. Здесь мы видим, что наша облачная функция - это сердце нашей логики. Он выполняет фактическое добавление, обновление, удаление и т. Д., И все, что делает наше приложение, - это выводит результат на нашу консоль.

Определите аргументы командной строки

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

На помощь приходит Commander.js. commander.js - это полное решение для интерфейсов командной строки node.js, вдохновленное Ruby commander.

/** contact.ts **/
...
commander
 .version('1.0.0')
 .description('Contact Management System')
commander
 .command('addContact')
 .alias('a')
 .description('Add a contact')
 .action(() => {
  console.log(chalk.yellow('=========*** Contact Management System ***=========='))
  inquirer.prompt(questions).then((answers) =>  actions.addContact(answers))
})
...

.command () Инициализировать новый Command.

Обратный вызов .action() вызывается, когда команда 'a' or 'addContact' указана через ARGV, а остальные аргументы применяются к функции для доступа.

Если аргумент команды равен «*», несоответствующая команда будет передана в качестве первого аргумента, за которым следует остальная часть оставшегося ARGV.

Вводимые пользователем данные во время выполнения

Следующая проблема после того, как мы закончим вышеизложенное, - это то, как мы получаем вводимые пользователем данные. Inquirer.js решает эту проблему за нас.

Inquirer.js стремится быть легко встраиваемым и красивым интерфейсом командной строки для Node.js (и, возможно, CLI« Xanadu ).

Inquirer.js должен облегчить процесс

  • предоставление обратной связи об ошибках
  • задавать вопросы
  • синтаксический анализ ввода
  • проверка ответов
  • управление иерархическими подсказками

Примечание. Inquirer.js предоставляет пользовательский интерфейс и поток сеанса запроса. Если вы ищете полноценную утилиту для командной строки, обратите внимание на commander, vorpal или args.

/** questions.ts **/
export let questions: Array<Object> = [
 {
  type: 'input',
  name: 'firstname',
  message: 'Enter first name'
 },
 {
  type: 'input',
  name: 'lastname',
  message: 'Enter Lastname'
 },
 {
  type: 'input',
  name: 'phone',
  message: 'Enter Phone Number'
 },
 {
  type: 'input',
  name: 'email',
  message: 'Enter Your Email Address'
 }
]
...

contact.ts

/** contact.ts **/
...
import { getIdQuestions, questions, updateContactQuestions } from './questions'
commander
 .command('addContact')
 .alias('a')
 .description('Add a contact')
 .action(() => {
  console.log(chalk.yellow('=========*** Contact Management System ***=========='))
  inquirer.prompt(questions).then((answers) =>  actions.addContact(answers))
})
...

Сюда импортируются функции контроллера в questions.ts. inquirer.prompt() запускает интерфейс подсказок (сеанс запроса), представляющий пользователю вопросы, переданные запрашивающему. Он возвращает обещание, answers, которое передается нашей функции контроллера addContact.

Сделайте наше приложение оболочкой Command

Теперь, когда наш инструмент готов, пришло время сделать его исполняемым, как обычную команду оболочки. Во-первых, давайте добавим shebang вверху contact.ts, который укажет оболочке, как выполнять этот сценарий.

/** contact.ts **/
#!/usr/bin/env node 
import './polyfills'
import * as commander from 'commander'

Теперь давайте настроим package.json, чтобы сделать его исполняемым.

"description": "A command-line utility to manage contacts",
"main": "index.js",
"preferGlobal": true,
"bin": "./contact.js",

Мы добавили новое свойство с именем bin, в котором мы указали имя команды, из которой будет выполняться contact.js.

Нам нужно скомпилировать наши скрипты на JavaScript, мы изменим наш package.json.

/** package.json **/
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon --exec ts-node -- contact.ts",
"ts-node": "ts-node contact.ts",
"build": "tsc"
},
...

Запустите npm run build.

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

npm install -g

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

contact --help

Это должно напечатать все доступные параметры, которые мы получаем после выполнения node contact --help. Теперь вы готовы представить миру свою утилиту.

Следует иметь в виду: во время разработки любые изменения, внесенные в проект, не будут видны, если вы просто выполните команду contact с заданными параметрами. Если вы запустите which contact, вы поймете, что путь к contact не совпадает с путем к проекту, в котором вы работаете. Чтобы этого не произошло, просто запустите npm link в папке проекта. Это автоматически установит символическую связь между исполняемой командой и каталогом проекта. Отныне любые изменения, которые вы вносите в каталог проекта, также будут отражаться в команде контакта.

Исходный код

Запустите наше приложение

- добавить контакт

- удалить контакт

- обновить контакт

- Список контактов

Заключение

Мы едва ли коснулись того, что возможно с инструментами командной строки в Node.js. Согласно Закону Этвуда существуют пакеты npm для элегантной обработки стандартного ввода, управления параллельными задачами, просмотра файлов, глобализации, сжатия, ssh, git и почти всего остального, что вы делали с Bash.

Исходный код для примера, который мы построили выше, свободно лицензирован и доступен на Github.

Если вы нашли это полезным, нашли ошибку или у вас есть другие интересные советы по созданию сценариев для Node.js, напишите мне в Twitter (я @ngArchangel).

Репозиторий Github

Вы можете найти полный исходный код в моем репозитории на Github.

Особая благодарность

Социальные медиа

Не стесняйтесь обращаться, если у вас возникнут проблемы.

Следуйте за мной в Medium и Twitter, чтобы узнать больше о TypeScript, JavaScript и Angular.