Недавно мое внимание привлекли две проблемы Node.js.

  1. В NPM легко найти реализации, но трудно найти решения.
    Например: легко найти синтаксический анализатор yaml, но сложно загрузить какой-либо заданный структурированный файл.
  2. Если вы найдете решение, у него может быть много зависимостей.
    Это делает установку тяжелой и медленной.

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

TL; DR: если ваш пакет становится слишком большим и имеет слишком много зависимостей, вы можете рассмотреть возможность использования PackageMissingError или require-implementation для улучшения жизни пользователей.

Кодирование для решений

Реализовать такой алгоритм, как синтаксический анализ файла yaml, легко.
Существует формат файла, называемый YAML, и форматы файлов предназначены для анализа.
Поиск пакета, который анализирует YAML, просто с NPM.

Однако: Обычно это не задача.

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

Теперь давайте продолжим, представив, что вам нужно написать пакет, который «загружает конфигурацию».

Задача: реализовать «load-config»

Какой формат конфигурации вы бы выбрали? JSON? ЯМЛ? ЦСОН? ".характеристики"? ИНИ? XML? ПЛИСТ? .CFG?
Некоторые из них имеют длинный набор зависимостей.

До сих пор я знал следующие подходы к выбору реализации:

а.) Выберите один формат и следуйте ему.

var loadConfig = require(‘load-json-config’)
loadConfig('./config.json')

Проблемы:

  1. Это приводит к появлению пакетов «load-json-config», «load-cson-config»,…, которые рассылают спам NPM и затрудняют поиск чего-либо.
  2. API может быть совершенно разным для каждой реализации. В этом случае пользователь будет жестко привязан к JSON, если он будет использовать «load-json-config».

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

var loadConfig = require(‘load-json-config’)
var jsYaml = require(‘js-yaml’)
loadConfig(‘./config.yaml’, {
  parse: function (blob){
    return jsYaml.load(blob.toString())
  }
})

Проблемы:

  1. Реализация по умолчанию всегда поставляется с кодом. (раздувание установки)
  2. Каждому новому пользователю нужно подумать о том, как удовлетворить ваш API.
  3. Вы можете не думать об одном конкретном способе реализации API. Но поскольку API является общедоступным, будет сложно обновить спецификацию.
    т.е. Некоторые анализаторы файлов создаются асинхронно.

c.) Не выбирайте формат, пусть решает пользователь.

var loadConfig = require(‘load-config’)
loadConfig(‘./config.json’, {
  parse: function (blob) {
    return JSON.parse(blob)
  }
})

Проблемы:

  1. То же, что и b.), у вас есть проблема открытого API, и каждому пользователю нужно еще раз подумать
  2. Ваша реализация не будет удобна для каждого конкретного случая.
  3. Пакеты, подобные «load-json-config», будут отображать всплывающие окна, которые просто заполняют
    «parse» -config. Это спамило бы NPM даже больше, чем a.).

г) Реализуйте каждый понятный для вас формат.

var loadConfig = require(‘load-config’)
loadConfig('./config.yaml')

Проблемы:

  1. Чтобы правильно реализовать все файлы конфигурации, потребуется много времени.
  2. Загрузка всех возможных зависимостей потребует большой полосы пропускания (при установке пакета) и ресурсов (на жестком диске).

Интерфейс против реализации

Когда я думаю об этой проблеме, я думаю об интерфейсах. В других языках программирования (например, TypeScript или Java) давно существует понятие интерфейса. Интерфейс в лучшем случае описывает упрощение способа решения проблемы (псевдосинтаксис).

interface Config {
  load (path:String):Object
}

Вы можете разобраться с фактической реализацией на более позднем этапе. Итак: хотя в нашем коде мы зависим от «как это работает», мы не зависим от «как это реализовано». Это значительно снижает головную боль.
В нашем случае config: loading должен описывать интерфейс. Это прекрасный способ написать c.). Тогда будет сложно поддерживать общедоступные интерфейсы, но это может быть хорошим способом реализации d.).

А где же новинка?

Лучший мир

Предположим, мы используем простейший подход d.):

var loadConfig = require(‘load-config’) 
loadConfig.load(‘./config.yaml’)

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

EPACKAGEMISSING: Trying to load yaml file.
This error can be easily fixed by running the following command:
$ npm install --save js-yaml

Это было бы круто! Теперь пользователь знает, что он в нужном месте, и может исправить это, запустив простой скрипт.

Как могла бы выглядеть реализация «load-config»?

Давайте воспользуемся тем фактом, что Node.js выполняет резервное копирование иерархии папок, чтобы проверить наличие пакета. Это означает, что load-config может получить доступ к пакету типа js-yaml без его установки как собственной зависимости, достаточно, если он установлен в родительской папке. !

var impl = require(‘require-implementation’)
function loadConfig (filePath) {
  if (path.extname(filePath) === '.yml') {
    var yaml = impl('Trying to load yaml file.').require('js-yaml')
    return yaml.load(fs.readFileSync(filePath, 'utf-8'))
  }
  return JSON.parse(filePath)
}

Require-implementation - это простой свежий пакет, который проверяет, существует ли js-yaml, а если нет, выдает то приятное сообщение об ошибке, упомянутое выше. Теперь всем понятно, как исправить недостающую зависимость.

Применение к другим задачам

В этом примере используются файлы конфигурации, но, похоже, он применяется, когда:

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

Могу представить и другие примеры:

  • Загрузчик изображений, который поддерживает большое количество форматов изображений. (Загружаемое изображение имеет один формат. Его также можно применить к другим форматам: например, к загрузчику 3D-моделей, текстовым документам, файлам презентаций…)
  • Инструмент командной строки, который хочет поддерживать перевод пользовательского интерфейса на различные языки.
    (Обычно вы хотите читать на одном языке.)
  • API-интерфейс геймпада с длинным списком различных драйверов геймпада.
    (Обычно подключен только один джойстик.)
  • Инструмент построения запросов с драйверами для разных баз данных.
    (В среднем сервер подключается к одному, может быть, двум типам баз данных.)
  • Механизм рендеринга HTML-шаблонов, который хочет предложить все существующие языки шаблонов ™. (см .: https://github.com/tj/consolidate.js)
    (Для здравомыслия ваших кодеров: используйте один язык шаблонов)

Заключительные слова

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

Это не решение для всего. Я уважаю сообщество Node.js за невероятное количество пакетов, которые хорошо решают одну задачу. Фактически, я думаю, что создание такого мета-пакета возможно только при использовании большого количества небольших, хорошо собранных пакетов.

Но как пользователь: вместо того, чтобы копаться в тоннах документации и искать / сравнивать решения на NPM, процесс стал бы намного проще:

  1. Загрузите пакет решения (например: load-config).
  2. Запустите это против своего кода.
  3. Установите зависимость, позволяющую реализовать ваши требования.

Для меня это может быть очень приятный процесс. Лучше, чем текущее решение методом проб и ошибок с NPM.

Что вы думаете?

Не могли бы вы присоединиться к моим усилиям, чтобы написать общий пакет структурированного для чтения файла?

[РЕДАКТИРОВАТЬ]: Я открыл выпуск # 13869 в NPM с просьбой о надлежащей поддержке этого.