Как получить данные из стороннего API с помощью Deno

В этой статье мы рассмотрим Deno, относительно новый инструмент, созданный в качестве конкурента / замены Node.js, который предлагает более безопасную среду и поставляется с поддержкой TypeScript из коробки.

Мы будем использовать Deno для создания инструмента командной строки для выполнения запросов к стороннему API — API Звёздных войн — и посмотрим, какие функции предоставляет Deno, чем он отличается от Node и как с ним работать.

Deno — это более самоуверенная среда выполнения, написанная на TypeScript, включающая собственный модуль форматирования кода ( deno fmt) и использующая модули ES — без каких-либо requireзаявлений CommonJS в поле зрения. По умолчанию он также чрезвычайно безопасен: вы должны явно предоставить своему коду разрешение на выполнение сетевых запросов или чтение файлов с дисков, что Node позволяет программам делать по умолчанию. В этой статье мы рассмотрим установку Deno, настройку нашей среды и создание простого приложения командной строки для выполнения запросов API.

Установка Deno

Вы можете проверить веб-сайт Deno для получения полных инструкций. Если вы используете macOS или Linux, вы можете скопировать эту команду в свой терминал:

curl -fsSL https://deno.land/x/install/install.sh | sh

Вам также необходимо добавить установочный каталог в ваш $PATH.

Не волнуйтесь, если вы работаете в Windows, так как вы можете установить Deno через менеджеры пакетов, такие как Chocolatey:

choco install deno

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

Вы можете проверить, установлен ли Deno, выполнив следующую команду:

deno -V

Это должно вывести версию Deno. На момент написания последней версии была 1.7.5, которую я использую.

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

Обратите внимание: если вы используете VS Code, по умолчанию плагин Deno не включается при загрузке проекта. Вы должны создать.vscode/settings.jsonфайл в своём репозитории и добавить следующее, чтобы включить плагин:

{
  "deno.enable": true
}

Опять же, если вы не являетесь пользователем VS Code, ознакомьтесь с приведённым выше руководством, чтобы найти правильную настройку для выбранного вами редактора.

Написание нашего первого сценария

Убедитесь, что у нас есть Deno и работает. Создайте index.tsи поместите внутрь следующее:

console.log("hello world!");

Мы можем запустить это с помощью deno run index.ts:

$ deno run index.ts
Check file:///home/jack/git/deno-star-wars-api/index.ts
hello world

Обратите внимание, что мы можем увидеть ошибку TypeScript в нашем редакторе:

'index.ts' cannot be compiled under '--isolatedModules' 
because it is considered a global script file. Add an import, 
export, or an empty 'export {}' statement 
to make it a module.ts(1208)

Эта ошибка возникает из-за того, что TypeScript не знает, что этот файл будет использовать импорт модуля ES. Скоро это произойдёт, потому что мы собираемся добавить импорт, но пока что, если мы хотим удалить ошибку, мы можем добавить пустой exportоператор в конец скрипта:

export {}

Это убедит компилятор TypeScript в том, что мы используем модули ES, и избавится от ошибки. Я не буду включать это в какие-либо примеры кода в сообщении в блоге, но это ничего не изменит, если мы добавим его, кроме устранения шума TypeScript.

Получение в Deno

Deno реализует поддержку того же Fetch API, который мы привыкли использовать в браузере. Он встроен в Deno, а это значит, что нет пакета для установки или настройки. Давайте посмотрим, как это работает, сделав наш первый запрос к API, который мы собираемся использовать здесь, API Звёздных войн (или SWAPI).

Отправив запрос, https://swapi.dev/api/people/1/мы вернём нам все данные, необходимые для Люка Скайуокера. Давайте обновим наш index.tsфайл, чтобы сделать этот запрос. Обновите, index.tsчтобы выглядеть так:

const json = fetch("https://swapi.dev/api/people/1");

json.then((response) => {
  return response.json();
}).then((data) => {
  console.log(data);
});

Попробуйте запустить это в своем терминале с помощью deno run:

$ deno run index.ts
Check file:///home/jack/git/deno-star-wars-api/index.ts
error: Uncaught (in promise) PermissionDenied: network access to "swapi.dev", run again with the --allow-net flag
    throw new ErrorClass(res.err.message);

Deno по умолчанию безопасен, а это означает, что скриптам требуется разрешение на всё, что может считаться опасным, например чтение / запись в файловую систему и выполнение сетевых запросов. Мы должны предоставить скриптам Deno разрешения при их запуске, чтобы позволить им выполнять такие действия. Мы можем включить наш с помощью —allow-netфлага:

$ deno run --allow-net index.ts
Check file:///home/jack/git/deno-star-wars-api/index.ts
{
  name: "Luke Skywalker",
  ...(data snipped to save space)...
}

Но этот флаг даёт скрипту разрешение на доступ к любому URL-адресу. Мы можем быть более явными и разрешить нашему скрипту получать доступ только к URL-адресам, которые мы добавляем в список разрешений:

$ deno run --allow-net=swapi.dev index.ts

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

$ deno run --allow-net=swapi.dev index.ts

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

const response = await fetch("https://swapi.dev/api/people/1/");
const data = await response.json();
console.log(data);

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

Установка сторонних зависимостей

Теперь, когда мы можем делать запросы к API Звёздных войн, давайте начнём думать о том, как мы хотим разрешить нашим пользователям использовать этот API. Мы предоставим флаги командной строки, чтобы они могли указать, какой ресурс запрашивать (например, люди, фильмы или планеты), и запрос для их фильтрации. Таким образом, вызов нашего инструмента командной строки может выглядеть так:

$ deno run --allow-net=swapi.dev index.ts --resource=people --query=luke

Мы могли бы проанализировать эти дополнительные аргументы командной строки вручную или использовать стороннюю библиотеку. В Node.js лучшим решением для этого является Yargs, и Yargs также поддерживает Deno, поэтому мы можем использовать Yargs для синтаксического анализа и работы с флагами командной строки, которые мы хотим поддерживать.

Однако для Deno нет диспетчера пакетов. Мы не создаём package.jsonи не устанавливаем зависимости. Вместо этого мы импортируем из URL-адресов. Лучший источник пакетов Deno — это репозиторий пакетов Deno, где вы можете найти нужный вам пакет. Большинство популярных пакетов npm теперь также поддерживают Deno, поэтому обычно есть хороший выбор и высокая вероятность того, что вы найдёте то, что вам нужно.

Читайте также:  Внешние переменные и функции в C

На момент написания поиск yargsв репозитории Deno даёт мне yargs 16.2.0. Чтобы использовать его локально, мы должны импортировать его из его URL:

import yargs from "https://deno.land/x/yargs/deno.ts";

Когда мы запустим наш скрипт, мы сначала увидим много вывода:

$ deno run --allow-net=swapi.dev index.ts
Download https://deno.land/x/yargs/deno.ts
Warning Implicitly using latest version (v16.2.0-deno) for https://deno.land/x/yargs/deno.ts
Download https://deno.land/x/yargs@v16.2.0-deno/deno.ts
Download https://deno.land/x/yargs@v16.2.0-deno/build/lib/yargs-factory.js
Download https://deno.land/x/yargs@v16.2.0-deno/lib/platform-shims/deno.ts
Download https://deno.land/std/path/mod.ts
Download https://deno.land/x/yargs_parser@v20.2.4-deno/deno.ts
...(more output removed to save space)

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

Обратите внимание на эту строку из приведённого выше вывода:

Warning Implicitly using latest version (v16.2.0-deno) 
for https://deno.land/x/yargs/deno.ts

Это Deno сообщает нам, что мы не указали конкретную версию при импорте Yargs, поэтому он просто загрузил последнюю. Это, вероятно, подходит для быстрых побочных проектов, но в целом рекомендуется закрепить наш импорт на той версии, которую мы хотели бы использовать. Мы можем сделать это, обновив URL:

import yargs from "https://deno.land/x/yargs@v16.2.0-deno/deno.ts";

Мне потребовалось время, чтобы вычислить этот URL. Я нашёл его, узнав, что при поиске «yargs» в репозитории Deno меня перенаправляют https://deno.land/x/yargs@v16.2.0-deno. Затем я посмотрел на вывод консоли и понял, что Deno действительно дал мне точный путь:

Warning Implicitly using latest version (v16.2.0-deno) 
for https://deno.land/x/yargs/deno.ts
Download https://deno.land/x/yargs@v16.2.0-deno/deno.ts

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

Обозначить fmt

Небольшое отступление, прежде чем мы продолжим создание нашего инструмента командной строки. Deno поставляется со встроенным средством форматирования, deno fmtкоторое автоматически форматирует код до единого стиля. Думайте об этом как о Prettier, но специально для Deno и встроенном. Это ещё одна причина, по которой меня привлекает Deno; Мне нравятся инструменты, которые предоставляют всё это прямо из коробки, без необходимости что-либо настраивать.

Мы можем запустить средство форматирования локально с помощью этого:

$ deno fmt

Это отформатирует все файлы JS и TS в текущем каталоге, или мы можем дать ему имя файла для форматирования:

$ deno fmt index.ts

Или, если у нас есть расширение VS Code, мы можем вместо этого перейти туда.vscode/settings.json, где мы включили плагин Deno ранее, и добавить эти две строки:

{
  "deno.enable": true,
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "denoland.vscode-deno"
}

Это настраивает VS Code на deno fmtавтоматический запуск при сохранении файла. Идеально!

Использование Yargs

Я не буду вдаваться в подробности Yargs (вы можете прочитать документацию, если хотите ознакомиться со всем, что он может делать), но вот как мы заявляем, что хотели бы использовать два аргумента командной строки которые необходимы: —resourceи —query:

import yargs from "https://deno.land/x/yargs@v16.2.0-deno/deno.ts";

const userArguments: {
  query: string;
  resource: "films" | "people" | "planets";
} = yargs(Deno.args)
  .describe("resource", "the type of resource from SWAPI to query for")
  .choices("resource", ["people", "films", "planets"])
  .describe("query", "the search term to query the SWAPI for")
  .demandOption(["resource", "query"])
  .argv;

console.log(userArguments);

Примечание: теперь, когда у нас есть importинструкция, нам больше не нужно export {}заглушать эту ошибку TypeScript.

К сожалению, на момент написания TypeScript, похоже, не улавливал все определения типов: для возвращаемого типа yargs(Deno.args)установлено значение {}, поэтому давайте немного поправим это. Мы можем определить наш собственный интерфейс TypeScript, который охватывает все части Yargs API, на которые мы полагаемся:

interface Yargs<ArgvReturnType> {
  describe: (param: string, description: string) => Yargs<ArgvReturnType>;
  choices: (param: string, options: string[]) => Yargs<ArgvReturnType>;
  demandOption: (required: string[]) => Yargs<ArgvReturnType>;
  argv: ArgvReturnType;
}

Здесь я объявляю функции, которые мы используем, и что они возвращают тот же интерфейс Yargs (это то, что позволяет нам связывать вызовы). Я также беру общий тип, ArgvReturnTypeкоторый обозначает структуру аргументов, которые мы возвращаем после того, как Яргс их обработал. Это означает, что я могу объявить UserArgumentsтип и передать ему результат yargs(Deno.argv):

interface Yargs<ArgvReturnType> {
  describe: (param: string, description: string) => Yargs<ArgvReturnType>;
  choices: (param: string, options: string[]) => Yargs<ArgvReturnType>;
  demandOption: (required: string[]) => Yargs<ArgvReturnType>;
  argv: ArgvReturnType;
}

interface UserArguments {
  query: string;
  resource: "films" | "people" | "planets";
}

const userArguments = (yargs(Deno.args) as Yargs<UserArguments>)
  .describe("resource", "the type of resource from SWAPI to query for")
  .choices("resource", ["people", "films", "planets"])
  .describe("query", "the search term to query the SWAPI for")
  .demandOption(["resource", "query"])
  .argv;

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

Запрос API Звёздных войн

Теперь, когда у нас есть метод принятия ввода пользователя, давайте напишем функцию, которая принимает введённые данные и правильно запрашивает API Звёздных войн:

async function queryStarWarsAPI(
  resource: "films" | "people" | "planets",
  query: string,
): Promise<{
  count: number;
  results: object[];
}> {
  const url = `https://swapi.dev/api/${resource}/?search=${query}`;
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

Мы возьмём два аргумента: ресурс для поиска и сам поисковый запрос. Результат, который возвращает API Звёздных войн, вернёт объект, включающий count(количество результатов) и resultsмассив, который представляет собой массив всех подходящих ресурсов из нашего запроса API. Мы рассмотрим повышение безопасности типов этого позже в статье, но пока я пошёл, objectчтобы начать. Это не лучший тип для использования, так как он очень либерален, но иногда я предпочитаю, чтобы что-то работало, а затем улучшали типы позже.

Теперь у нас есть эта функция, мы можем взять аргументы, проанализированные Яргсом, и получить некоторые данные!

const result = await queryStarWarsAPI(
  userArguments.resource,
  userArguments.query,
);
console.log(`${result.count} results`);

Теперь запустим это:

$ deno run --allow-net=swapi.dev index.ts --resource films --query phantom
Check file:///home/jack/git/deno-star-wars-api/index.ts
1 results

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

Читайте также:  Поисковые алгоритмы Google

Первое, что нужно сделать, это создать новый тип, представляющий ресурсы, которые мы позволяем пользователю запрашивать:

type StarWarsResource = "films" | "people" | "planets";

Затем мы можем использовать этот тип, а не дублировать его, сначала когда мы передаём его в Yargs, а второй раз, когда мы определяем queryStarWarsAPIфункцию:

interface UserArguments {
  query: string;
  resource: StarWarsResource;
}

// ...

async function queryStarWarsAPI(
  resource: StarWarsResource,
  query: string,
): Promise<{
  count: number;
  results: object[];
}>  { ... }

Далее давайте взглянем на API Звёздных войн и создадим интерфейсы, представляющие то, что мы получим за разные ресурсы. Эти типы не являются исчерпывающими (API возвращает больше). Я только что выбрал несколько предметов для каждого ресурса:

interface Person {
  name: string;
  films: string[];
  height: string;
  mass: string;
  homeworld: string;
}

interface Film {
  title: string;
  episode_id: number;
  director: string;
  release_date: string;
}

interface Planet {
  name: string;
  terrain: string;
  population: string;
}

Когда у нас есть эти типы, мы можем создать функцию для обработки результатов для каждого типа, а затем вызвать её. Мы можем использовать приведение типов, чтобы сообщить TypeScript, что result.results(как он думает object[]) на самом деле является одним из наших типов интерфейса:

console.log(`${result.count} results`);

switch (userArguments.resource) {
  case "films": {
    logFilms(result.results as Film[]);
    break;
  }
  case "people": {
    logPeople(result.results as Person[]);
    break;
  }
  case "planets": {
    logPlanets(result.results as Planet[]);
    break;
  }
}

function logFilms(films: Film[]): void { ... }
function logPeople(people: Person[]): void { ... }
function logPlanets(planets: Planet[]): void { ... }

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

function logFilms(films: Film[]): void {
  films.forEach((film) => {
    console.log(film.title);
    console.log(`=> Directed by ${film.director}`);
    console.log(`=> Released on ${film.release_date}`);
  });
}
function logPeople(people: Person[]): void {
  people.forEach((person) => {
    console.log(person.name);
    console.log(`=> Height: ${person.height}`);
    console.log(`=> Mass:   ${person.mass}`);
  });
}
function logPlanets(planets: Planet[]): void {
  planets.forEach((planet) => {
    console.log(planet.name);
    console.log(`=> Terrain:      ${planet.terrain}`);
    console.log(`=> Population:   ${planet.population}`);
  });
}

Давайте наконец исправим тот факт, что он выводит, 1 resultsа не 1 result:

function pluralise(singular: string, plural: string, count: number): string {
  return `${count} ${count === 1 ? singular : plural}`;
}

console.log(pluralise("result", "results", result.count));

И теперь вывод нашего интерфейса командной строки выглядит хорошо!

$ deno run --allow-net=swapi.dev index.ts --resource planets --query tat
Check file:///home/jack/git/deno-star-wars-api/index.ts
1 result
Tatooine
=> Terrain:      desert
=> Population:   200000

Tidying Up

Прямо сейчас весь наш код представляет собой один большой index.tsфайл. Создадим api.tsфайл и перенесём в него большую часть логики API.

Не забудьте добавить exportв начало всех типов, интерфейсов и функций в этом файле, так как нам нужно будет импортировать их в index.ts:

// api.ts
export type StarWarsResource = "films" | "people" | "planets";

export interface Person {
  name: string;
  films: string[];
  height: string;
  mass: string;
  homeworld: string;
}

export interface Film {
  title: string;
  episode_id: number;
  director: string;
  release_date: string;
}

export interface Planet {
  name: string;
  terrain: string;
  population: string;
}

export async function queryStarWarsAPI(
  resource: StarWarsResource,
  query: string,
): Promise<{
  count: number;
  results: object[];
}> {
  const url = `https://swapi.dev/api/${resource}/?search=${query}`;
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

А затем мы можем импортировать их из index.ts:

import {
  Film,
  Person,
  Planet,
  queryStarWarsAPI,
  StarWarsResource,
} from "./api.ts"

Теперь наш index.tsвыглядит намного чище, и мы переместили все детали API в отдельный модуль.

Distributing

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

Мы можем использовать deno bundleдля объединения всего нашего кода в один файл JavaScript со всеми установленными зависимостями. Таким образом, совместное использование скрипта — это случай совместного использования одного файла:

$ deno bundle index.ts out.js

И мы можем передать этот скрипт deno.run, как и раньше. Разница теперь в том, что Deno не нужно выполнять какую-либо проверку типов или устанавливать какие-либо зависимости, потому что всё это было сделано out.jsза нас. Это означает, что запуск такого связанного скрипта, вероятно, будет быстрее, чем запуск из исходного кода TypeScript:

$ deno run --allow-net=swapi.dev out.js --resource films --query phantom
1 result
The Phantom Menace
=> Directed by George Lucas
=> Released on 1999-05-19

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

Мы можем бежать, deno compile —unstable —allow-net=swapi.dev index.tsчтобы попросить Deno создать для нас автономный исполняемый файл. —unstableФлаг требуется, потому что эта функция является экспериментальной, хотя в будущем это не должно быть. Что замечательно в этом, так это то, что мы передаём флаги безопасности во время компиляции — в нашем случае разрешая доступ к API Звёздных войн. Это означает, что если мы передадим этот исполняемый файл пользователю, ему не нужно будет знать о настройке флагов:

$ deno compile --unstable --allow-net=swapi.dev index.ts
Check file:///home/jack/git/deno-star-wars-api/index.ts
Bundle file:///home/jack/git/deno-star-wars-api/index.ts
Compile file:///home/jack/git/deno-star-wars-api/index.ts
Emit deno-star-wars-api

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

Вывод

В этой статье, создав инструмент CLI, мы узнали, как использовать Deno для извлечения данных из стороннего API и отображения результатов. Мы увидели, как Deno реализует поддержку того же Fetch API, который мы привыкли использовать в браузере, как fetchон встроен в стандартную библиотеку Deno и как мы можем использовать awaitна верхнем уровне нашей программы, не оборачивая всё в IFFE.

Надеюсь, вы согласитесь со мной, что в Дено есть что любить. Он обеспечивает очень продуктивную среду прямо из коробки, в комплекте с TypeScript и средством форматирования. Замечательно, что у менеджера пакетов нет накладных расходов, особенно при написании небольших вспомогательных инструментов, а возможность компиляции в один исполняемый файл означает, что делиться этими инструментами с коллегами и друзьями очень просто. просто.

Оцените статью
bestprogrammer.ru
Добавить комментарий